politty 0.7.0 → 0.9.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 (63) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/docs/index.d.ts +44 -2
  3. package/dist/docs/index.js +827 -43
  4. package/dist/index.js +1 -1
  5. package/dist/{runner-D43SkHt5.js → runner-APRZYXUS.js} +74 -3
  6. package/package.json +22 -67
  7. package/dist/arg-registry-DDJpsUea.d.cts +0 -942
  8. package/dist/arg-registry-DDJpsUea.d.cts.map +0 -1
  9. package/dist/arg-registry-DDJpsUea.d.ts.map +0 -1
  10. package/dist/augment.cjs +0 -0
  11. package/dist/augment.d.cts +0 -17
  12. package/dist/augment.d.cts.map +0 -1
  13. package/dist/augment.d.ts.map +0 -1
  14. package/dist/cli.cjs +0 -54
  15. package/dist/cli.cjs.map +0 -1
  16. package/dist/cli.d.cts +0 -1
  17. package/dist/cli.js.map +0 -1
  18. package/dist/completion/index.cjs +0 -23
  19. package/dist/completion/index.d.cts +0 -3
  20. package/dist/completion-CLHO3Xaz.cjs +0 -5769
  21. package/dist/completion-CLHO3Xaz.cjs.map +0 -1
  22. package/dist/completion-DHnVx9Zk.js.map +0 -1
  23. package/dist/docs/index.cjs +0 -2343
  24. package/dist/docs/index.cjs.map +0 -1
  25. package/dist/docs/index.d.cts +0 -710
  26. package/dist/docs/index.d.cts.map +0 -1
  27. package/dist/docs/index.d.ts.map +0 -1
  28. package/dist/docs/index.js.map +0 -1
  29. package/dist/index-DKGn3lIl.d.ts.map +0 -1
  30. package/dist/index-WyViqW59.d.cts +0 -663
  31. package/dist/index-WyViqW59.d.cts.map +0 -1
  32. package/dist/index.cjs +0 -45
  33. package/dist/index.d.cts +0 -685
  34. package/dist/index.d.cts.map +0 -1
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/log-collector-DK32-73m.js.map +0 -1
  37. package/dist/log-collector-DUqC427m.cjs +0 -185
  38. package/dist/log-collector-DUqC427m.cjs.map +0 -1
  39. package/dist/prompt/clack/index.cjs +0 -33
  40. package/dist/prompt/clack/index.cjs.map +0 -1
  41. package/dist/prompt/clack/index.d.cts +0 -18
  42. package/dist/prompt/clack/index.d.cts.map +0 -1
  43. package/dist/prompt/clack/index.d.ts.map +0 -1
  44. package/dist/prompt/clack/index.js.map +0 -1
  45. package/dist/prompt/index.cjs +0 -7
  46. package/dist/prompt/index.d.cts +0 -108
  47. package/dist/prompt/index.d.cts.map +0 -1
  48. package/dist/prompt/index.d.ts.map +0 -1
  49. package/dist/prompt/inquirer/index.cjs +0 -48
  50. package/dist/prompt/inquirer/index.cjs.map +0 -1
  51. package/dist/prompt/inquirer/index.d.cts +0 -18
  52. package/dist/prompt/inquirer/index.d.cts.map +0 -1
  53. package/dist/prompt/inquirer/index.d.ts.map +0 -1
  54. package/dist/prompt/inquirer/index.js.map +0 -1
  55. package/dist/prompt-Bs9e-Em3.cjs +0 -196
  56. package/dist/prompt-Bs9e-Em3.cjs.map +0 -1
  57. package/dist/prompt-Cc8Tfmdv.js.map +0 -1
  58. package/dist/runner-D43SkHt5.js.map +0 -1
  59. package/dist/runner-DvFvokV6.cjs +0 -2865
  60. package/dist/runner-DvFvokV6.cjs.map +0 -1
  61. package/dist/schema-extractor-BxSRwLrx.cjs +0 -710
  62. package/dist/schema-extractor-BxSRwLrx.cjs.map +0 -1
  63. package/dist/schema-extractor-Dqe7_kyQ.js.map +0 -1
@@ -1,2343 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_log_collector = require('../log-collector-DUqC427m.cjs');
3
- const require_schema_extractor = require('../schema-extractor-BxSRwLrx.cjs');
4
- let zod = require("zod");
5
- let node_fs = require("node:fs");
6
- node_fs = require_log_collector.__toESM(node_fs, 1);
7
- let node_path = require("node:path");
8
- node_path = require_log_collector.__toESM(node_path, 1);
9
- let node_util = require("node:util");
10
-
11
- //#region src/docs/types.ts
12
- /**
13
- * Environment variable name for update mode
14
- */
15
- const UPDATE_GOLDEN_ENV = "POLITTY_DOCS_UPDATE";
16
- /**
17
- * Environment variable name for doctor mode.
18
- * When enabled alone, detects and reports missing section markers (read-only).
19
- * When combined with POLITTY_DOCS_UPDATE=true, auto-inserts missing markers.
20
- */
21
- const DOCTOR_ENV = "POLITTY_DOCS_DOCTOR";
22
- /**
23
- * All section types in rendering order
24
- */
25
- const SECTION_TYPES = [
26
- "heading",
27
- "description",
28
- "usage",
29
- "arguments",
30
- "options",
31
- "global-options-link",
32
- "subcommands",
33
- "examples",
34
- "notes"
35
- ];
36
- /**
37
- * Marker prefix for command section markers in generated documentation
38
- * Format: <!-- politty:command:<scope>:<type>:start --> ... <!-- politty:command:<scope>:<type>:end -->
39
- */
40
- const SECTION_MARKER_PREFIX = "politty:command";
41
- /**
42
- * Generate start marker for a command section
43
- */
44
- function sectionStartMarker(type, scope) {
45
- return `<!-- ${SECTION_MARKER_PREFIX}:${scope}:${type}:start -->`;
46
- }
47
- /**
48
- * Generate end marker for a command section
49
- */
50
- function sectionEndMarker(type, scope) {
51
- return `<!-- ${SECTION_MARKER_PREFIX}:${scope}:${type}:end -->`;
52
- }
53
- /**
54
- * Marker prefix for global options sections in generated documentation
55
- * Format: <!-- politty:global-options:start --> ... <!-- politty:global-options:end -->
56
- */
57
- const GLOBAL_OPTIONS_MARKER_PREFIX = "politty:global-options";
58
- /**
59
- * Generate start marker for a global options section
60
- */
61
- function globalOptionsStartMarker() {
62
- return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:start -->`;
63
- }
64
- /**
65
- * Generate end marker for a global options section
66
- */
67
- function globalOptionsEndMarker() {
68
- return `<!-- ${GLOBAL_OPTIONS_MARKER_PREFIX}:end -->`;
69
- }
70
- /**
71
- * Marker prefix for root header sections in generated documentation
72
- */
73
- const ROOT_HEADER_MARKER_PREFIX = "politty:root-header";
74
- function rootHeaderStartMarker() {
75
- return `<!-- ${ROOT_HEADER_MARKER_PREFIX}:start -->`;
76
- }
77
- function rootHeaderEndMarker() {
78
- return `<!-- ${ROOT_HEADER_MARKER_PREFIX}:end -->`;
79
- }
80
- /**
81
- * Marker prefix for root footer sections in generated documentation
82
- */
83
- const ROOT_FOOTER_MARKER_PREFIX = "politty:root-footer";
84
- function rootFooterStartMarker() {
85
- return `<!-- ${ROOT_FOOTER_MARKER_PREFIX}:start -->`;
86
- }
87
- function rootFooterEndMarker() {
88
- return `<!-- ${ROOT_FOOTER_MARKER_PREFIX}:end -->`;
89
- }
90
- /**
91
- * Marker prefix for index sections in generated documentation
92
- * Format: <!-- politty:index:<scope>:start --> ... <!-- politty:index:<scope>:end -->
93
- */
94
- const INDEX_MARKER_PREFIX = "politty:index";
95
- /**
96
- * Generate start marker for an index section
97
- */
98
- function indexStartMarker(scope) {
99
- return `<!-- ${INDEX_MARKER_PREFIX}:${scope}:start -->`;
100
- }
101
- /**
102
- * Generate end marker for an index section
103
- */
104
- function indexEndMarker(scope) {
105
- return `<!-- ${INDEX_MARKER_PREFIX}:${scope}:end -->`;
106
- }
107
-
108
- //#endregion
109
- //#region src/docs/default-renderers.ts
110
- /**
111
- * Escape markdown special characters in table cells
112
- */
113
- function escapeTableCell$2(str) {
114
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
115
- }
116
- /**
117
- * Marker appended to a custom negation row/line so readers can see which
118
- * positive flag it negates (e.g. `--monochrome` → `(↔ \`--color\`)`).
119
- */
120
- function negationRelationMarker(opt) {
121
- return `(↔ \`--${opt.cliName}\`)`;
122
- }
123
- /**
124
- * Format default value for display
125
- */
126
- function formatDefaultValue$1(value) {
127
- if (value === void 0) return "-";
128
- return `\`${JSON.stringify(value)}\``;
129
- }
130
- /**
131
- * Render usage line
132
- */
133
- function renderUsage(info) {
134
- const parts = [info.fullCommandPath];
135
- if (info.options.length > 0) parts.push("[options]");
136
- if (info.subCommands.length > 0) parts.push("[command]");
137
- for (const arg of info.positionalArgs) if (arg.required) parts.push(`<${arg.name}>`);
138
- else parts.push(`[${arg.name}]`);
139
- return parts.join(" ");
140
- }
141
- /**
142
- * Render arguments as table
143
- */
144
- function renderArgumentsTable(info) {
145
- if (info.positionalArgs.length === 0) return "";
146
- const lines = [];
147
- lines.push("| Argument | Description | Required |");
148
- lines.push("|----------|-------------|----------|");
149
- for (const arg of info.positionalArgs) {
150
- const desc = escapeTableCell$2(arg.description ?? "");
151
- const required = arg.required ? "Yes" : "No";
152
- lines.push(`| \`${arg.name}\` | ${desc} | ${required} |`);
153
- }
154
- return lines.join("\n");
155
- }
156
- /**
157
- * Render arguments as list
158
- */
159
- function renderArgumentsList(info) {
160
- if (info.positionalArgs.length === 0) return "";
161
- const lines = [];
162
- for (const arg of info.positionalArgs) {
163
- const required = arg.required ? "(required)" : "(optional)";
164
- const desc = arg.description ? ` - ${arg.description}` : "";
165
- lines.push(`- \`${arg.name}\`${desc} ${required}`);
166
- }
167
- return lines.join("\n");
168
- }
169
- /**
170
- * Format environment variable info for display
171
- */
172
- function formatEnvInfo(env) {
173
- if (!env) return "";
174
- return ` [env: ${(Array.isArray(env) ? env : [env]).join(", ")}]`;
175
- }
176
- /**
177
- * Resolve placeholder for an option (uses kebab-case cliName)
178
- */
179
- function resolvePlaceholder(opt) {
180
- return opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
181
- }
182
- /**
183
- * Format option name for table display (e.g., `--dry-run` or `--port <PORT>`)
184
- *
185
- * Boolean fields with a custom inline `negation` (no separate description) are
186
- * shown as `\`--cache\` / \`--disable-cache\``.
187
- */
188
- function formatOptionName(opt) {
189
- const placeholder = resolvePlaceholder(opt);
190
- if (opt.type === "boolean") {
191
- const positive = `\`--${opt.cliName}\``;
192
- if (opt.negationDisplay && !opt.negationDescription) return `${positive} / \`--${opt.negationDisplay}\``;
193
- return positive;
194
- }
195
- return `\`--${opt.cliName} <${placeholder}>\``;
196
- }
197
- /**
198
- * Format option flags for list display (uses kebab-case cliName).
199
- * Aliases are joined with `, `; the inline negation (when no separate
200
- * `negationDescription` is set) is appended with ` / ` so it stays
201
- * visually distinct from aliases, matching help and table output.
202
- */
203
- function formatOptionFlags(opt) {
204
- const placeholder = resolvePlaceholder(opt);
205
- const longFlag = opt.type === "boolean" ? `--${opt.cliName}` : `--${opt.cliName} <${placeholder}>`;
206
- const parts = [];
207
- if (opt.alias) {
208
- for (const a of opt.alias) if (a.length === 1) parts.push(`\`-${a}\``);
209
- }
210
- parts.push(`\`${longFlag}\``);
211
- if (opt.alias) {
212
- for (const a of opt.alias) if (a.length > 1) parts.push(`\`--${a}\``);
213
- }
214
- const aliasJoined = parts.join(", ");
215
- if (opt.type === "boolean" && opt.negationDisplay && !opt.negationDescription) return `${aliasJoined} / \`--${opt.negationDisplay}\``;
216
- return aliasJoined;
217
- }
218
- /**
219
- * Format aliases for a markdown table cell
220
- */
221
- function formatAliasCell(alias) {
222
- if (!alias || alias.length === 0) return "-";
223
- return alias.map((a) => `\`${a.length === 1 ? `-${a}` : `--${a}`}\``).join(", ");
224
- }
225
- /**
226
- * Format env variable names for table display
227
- */
228
- function formatEnvNames(env) {
229
- if (!env) return "-";
230
- if (Array.isArray(env)) return env.map((e) => `\`${e}\``).join(", ");
231
- return `\`${env}\``;
232
- }
233
- /**
234
- * Render options as markdown table
235
- *
236
- * Features:
237
- * - Uses kebab-case (cliName) for option names (e.g., `--dry-run` instead of `--dryRun`)
238
- * - Automatically adds Env column when any option has env configured
239
- * - Displays multiple env vars as comma-separated list
240
- *
241
- * @example
242
- * | Option | Alias | Description | Required | Default | Env |
243
- * |--------|-------|-------------|----------|---------|-----|
244
- * | `--dry-run` | `-d` | Dry run mode | No | `false` | - |
245
- * | `--port <PORT>` | - | Server port | Yes | - | `PORT`, `SERVER_PORT` |
246
- */
247
- function renderOptionsTable(info) {
248
- if (info.options.length === 0) return "";
249
- const hasEnv = info.options.some((opt) => opt.env);
250
- const lines = [];
251
- if (hasEnv) {
252
- lines.push("| Option | Alias | Description | Required | Default | Env |");
253
- lines.push("|--------|-------|-------------|----------|---------|-----|");
254
- } else {
255
- lines.push("| Option | Alias | Description | Required | Default |");
256
- lines.push("|--------|-------|-------------|----------|---------|");
257
- }
258
- for (const opt of info.options) {
259
- const optionName = formatOptionName(opt);
260
- const alias = formatAliasCell(opt.alias);
261
- const desc = escapeTableCell$2(opt.description ?? "");
262
- const required = opt.required ? "Yes" : "No";
263
- const defaultVal = formatDefaultValue$1(opt.defaultValue);
264
- if (hasEnv) {
265
- const envNames = formatEnvNames(opt.env);
266
- lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} | ${envNames} |`);
267
- } else lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} |`);
268
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
269
- const negName = `\`--${opt.negationDisplay}\``;
270
- const negDesc = `${escapeTableCell$2(opt.negationDescription)} ${negationRelationMarker(opt)}`;
271
- if (hasEnv) lines.push(`| ${negName} | - | ${negDesc} | ${required} | - | - |`);
272
- else lines.push(`| ${negName} | - | ${negDesc} | ${required} | - |`);
273
- }
274
- }
275
- return lines.join("\n");
276
- }
277
- /**
278
- * Render options as markdown list
279
- *
280
- * Features:
281
- * - Uses kebab-case (cliName) for option names (e.g., `--dry-run` instead of `--dryRun`)
282
- * - Appends env info at the end of each option (e.g., `[env: PORT, SERVER_PORT]`)
283
- *
284
- * @example
285
- * - `-d`, `--dry-run` - Dry run mode (default: false)
286
- * - `--port <PORT>` - Server port (required) [env: PORT, SERVER_PORT]
287
- */
288
- function renderOptionsList(info) {
289
- if (info.options.length === 0) return "";
290
- const lines = [];
291
- for (const opt of info.options) {
292
- const flags = formatOptionFlags(opt);
293
- const desc = opt.description ? ` - ${opt.description}` : "";
294
- const required = opt.required ? " (required)" : "";
295
- const defaultVal = opt.defaultValue !== void 0 ? ` (default: ${JSON.stringify(opt.defaultValue)})` : "";
296
- const envInfo = formatEnvInfo(opt.env);
297
- lines.push(`- ${flags}${desc}${required}${defaultVal}${envInfo}`);
298
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) lines.push(`- \`--${opt.negationDisplay}\` - ${opt.negationDescription} ${negationRelationMarker(opt)}`);
299
- }
300
- return lines.join("\n");
301
- }
302
- /**
303
- * Generate anchor from command path
304
- */
305
- function generateAnchor$1(commandPath) {
306
- return commandPath.join("-").toLowerCase();
307
- }
308
- /**
309
- * Generate relative path from one file to another.
310
- * Always emits forward slashes so Markdown links remain portable across OSes.
311
- */
312
- function getRelativePath(from, to) {
313
- const fromPosix = from.replace(/\\/g, "/");
314
- const toPosix = to.replace(/\\/g, "/");
315
- return node_path.default.posix.relative(node_path.default.posix.dirname(fromPosix), toPosix);
316
- }
317
- /**
318
- * Render subcommands as table
319
- */
320
- function renderSubcommandsTable(info, generateAnchors = true) {
321
- return renderSubcommandsTableFromArray(info.subCommands, info, generateAnchors);
322
- }
323
- /**
324
- * Render options from array as table
325
- */
326
- function renderOptionsTableFromArray(options) {
327
- if (options.length === 0) return "";
328
- const hasEnv = options.some((opt) => opt.env);
329
- const lines = [];
330
- if (hasEnv) {
331
- lines.push("| Option | Alias | Description | Required | Default | Env |");
332
- lines.push("|--------|-------|-------------|----------|---------|-----|");
333
- } else {
334
- lines.push("| Option | Alias | Description | Required | Default |");
335
- lines.push("|--------|-------|-------------|----------|---------|");
336
- }
337
- for (const opt of options) {
338
- const optionName = formatOptionName(opt);
339
- const alias = formatAliasCell(opt.alias);
340
- const desc = escapeTableCell$2(opt.description ?? "");
341
- const required = opt.required ? "Yes" : "No";
342
- const defaultVal = formatDefaultValue$1(opt.defaultValue);
343
- if (hasEnv) {
344
- const envNames = formatEnvNames(opt.env);
345
- lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} | ${envNames} |`);
346
- } else lines.push(`| ${optionName} | ${alias} | ${desc} | ${required} | ${defaultVal} |`);
347
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
348
- const negName = `\`--${opt.negationDisplay}\``;
349
- const negDesc = `${escapeTableCell$2(opt.negationDescription)} ${negationRelationMarker(opt)}`;
350
- if (hasEnv) lines.push(`| ${negName} | - | ${negDesc} | ${required} | - | - |`);
351
- else lines.push(`| ${negName} | - | ${negDesc} | ${required} | - |`);
352
- }
353
- }
354
- return lines.join("\n");
355
- }
356
- /**
357
- * Render union/xor options as markdown with variant grouping
358
- */
359
- function renderUnionOptionsMarkdown(extracted, style = "table") {
360
- const unionOptions = extracted.unionOptions ?? [];
361
- if (unionOptions.length === 0) return "";
362
- const sections = [];
363
- const allFieldNames = /* @__PURE__ */ new Set();
364
- for (const option of unionOptions) for (const field of option.fields) allFieldNames.add(field.name);
365
- const commonFieldNames = /* @__PURE__ */ new Set();
366
- for (const fieldName of allFieldNames) if (unionOptions.every((o) => o.fields.some((f) => f.name === fieldName))) commonFieldNames.add(fieldName);
367
- const commonFields = extracted.fields.filter((f) => commonFieldNames.has(f.name) && !f.positional);
368
- if (commonFields.length > 0) sections.push(style === "table" ? renderOptionsTableFromArray(commonFields) : renderOptionsListFromArray(commonFields));
369
- sections.push("> One of the following option groups is required:");
370
- for (let i = 0; i < unionOptions.length; i++) {
371
- const option = unionOptions[i];
372
- if (!option) continue;
373
- const uniqueFields = option.fields.filter((f) => !commonFieldNames.has(f.name) && !f.positional);
374
- const label = option.description ?? `Variant ${i + 1}`;
375
- if (uniqueFields.length === 0) {
376
- sections.push(`**${label}:**\n\n_no options_`);
377
- continue;
378
- }
379
- const rendered = style === "table" ? renderOptionsTableFromArray(uniqueFields) : renderOptionsListFromArray(uniqueFields);
380
- sections.push(`**${label}:**\n\n${rendered}`);
381
- }
382
- return sections.join("\n\n");
383
- }
384
- /**
385
- * Render discriminatedUnion options as markdown with variant grouping
386
- */
387
- function renderDiscriminatedUnionOptionsMarkdown(extracted, style = "table") {
388
- const discriminator = extracted.discriminator;
389
- const variants = extracted.variants ?? [];
390
- if (!discriminator || variants.length === 0) return "";
391
- const sections = [];
392
- const allFieldNames = /* @__PURE__ */ new Set();
393
- for (const variant of variants) for (const field of variant.fields) allFieldNames.add(field.name);
394
- const commonFieldNames = /* @__PURE__ */ new Set();
395
- for (const fieldName of allFieldNames) {
396
- if (fieldName === discriminator) continue;
397
- if (variants.every((v) => v.fields.some((f) => f.name === fieldName))) commonFieldNames.add(fieldName);
398
- }
399
- const discriminatorField = extracted.fields.find((f) => f.name === discriminator);
400
- const variantValues = variants.map((v) => v.discriminatorValue).join("\\|");
401
- const topFields = [];
402
- if (discriminatorField) topFields.push({
403
- ...discriminatorField,
404
- placeholder: variantValues
405
- });
406
- for (const fieldName of commonFieldNames) {
407
- const field = extracted.fields.find((f) => f.name === fieldName);
408
- if (field && !field.positional) topFields.push(field);
409
- }
410
- if (topFields.length > 0) sections.push(style === "table" ? renderOptionsTableFromArray(topFields) : renderOptionsListFromArray(topFields));
411
- for (const variant of variants) {
412
- const uniqueFields = variant.fields.filter((f) => f.name !== discriminator && !commonFieldNames.has(f.name) && !f.positional);
413
- if (uniqueFields.length === 0) continue;
414
- const descSuffix = variant.description ? ` ${variant.description}` : "";
415
- const label = `**When \`${discriminator}\` = \`${variant.discriminatorValue}\`:**${descSuffix}`;
416
- const rendered = style === "table" ? renderOptionsTableFromArray(uniqueFields) : renderOptionsListFromArray(uniqueFields);
417
- sections.push(`${label}\n\n${rendered}`);
418
- }
419
- return sections.join("\n\n");
420
- }
421
- /**
422
- * Render options from array as list
423
- */
424
- function renderOptionsListFromArray(options) {
425
- if (options.length === 0) return "";
426
- const lines = [];
427
- for (const opt of options) {
428
- const flags = formatOptionFlags(opt);
429
- const desc = opt.description ? ` - ${opt.description}` : "";
430
- const required = opt.required ? " (required)" : "";
431
- const defaultVal = opt.defaultValue !== void 0 ? ` (default: ${JSON.stringify(opt.defaultValue)})` : "";
432
- const envInfo = formatEnvInfo(opt.env);
433
- lines.push(`- ${flags}${desc}${required}${defaultVal}${envInfo}`);
434
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) lines.push(`- \`--${opt.negationDisplay}\` - ${opt.negationDescription} ${negationRelationMarker(opt)}`);
435
- }
436
- return lines.join("\n");
437
- }
438
- /**
439
- * Render arguments from array as table
440
- */
441
- function renderArgumentsTableFromArray(args) {
442
- if (args.length === 0) return "";
443
- const lines = [];
444
- lines.push("| Argument | Description | Required |");
445
- lines.push("|----------|-------------|----------|");
446
- for (const arg of args) {
447
- const desc = escapeTableCell$2(arg.description ?? "");
448
- const required = arg.required ? "Yes" : "No";
449
- lines.push(`| \`${arg.name}\` | ${desc} | ${required} |`);
450
- }
451
- return lines.join("\n");
452
- }
453
- /**
454
- * Render arguments from array as list
455
- */
456
- function renderArgumentsListFromArray(args) {
457
- if (args.length === 0) return "";
458
- const lines = [];
459
- for (const arg of args) {
460
- const required = arg.required ? "(required)" : "(optional)";
461
- const desc = arg.description ? ` - ${arg.description}` : "";
462
- lines.push(`- \`${arg.name}\`${desc} ${required}`);
463
- }
464
- return lines.join("\n");
465
- }
466
- /**
467
- * Render subcommands from array as table
468
- */
469
- function renderSubcommandsTableFromArray(subcommands, info, generateAnchors = true) {
470
- if (subcommands.length === 0) return "";
471
- const hasAliases = subcommands.some((s) => s.aliases && s.aliases.length > 0);
472
- const lines = [];
473
- if (hasAliases) {
474
- lines.push("| Command | Aliases | Description |");
475
- lines.push("|---------|---------|-------------|");
476
- } else {
477
- lines.push("| Command | Description |");
478
- lines.push("|---------|-------------|");
479
- }
480
- const currentFile = info.filePath;
481
- const fileMap = info.fileMap;
482
- for (const sub of subcommands) {
483
- const fullName = sub.fullPath.join(" ");
484
- const desc = escapeTableCell$2(sub.description ?? "");
485
- const subCommandPath = sub.fullPath.join(" ");
486
- const aliasCell = hasAliases ? sub.aliases && sub.aliases.length > 0 ? sub.aliases.map((a) => `\`${escapeTableCell$2(a)}\``).join(", ") : "-" : "";
487
- let cmdCell;
488
- if (generateAnchors) {
489
- const anchor = generateAnchor$1(sub.fullPath);
490
- const subFile = fileMap?.[subCommandPath];
491
- if (currentFile && subFile && currentFile !== subFile) cmdCell = `[\`${fullName}\`](${getRelativePath(currentFile, subFile)}#${anchor})`;
492
- else cmdCell = `[\`${fullName}\`](#${anchor})`;
493
- } else cmdCell = `\`${fullName}\``;
494
- if (hasAliases) lines.push(`| ${cmdCell} | ${aliasCell} | ${desc} |`);
495
- else lines.push(`| ${cmdCell} | ${desc} |`);
496
- }
497
- return lines.join("\n");
498
- }
499
- /**
500
- * Render examples as markdown
501
- *
502
- * @example
503
- * **Basic usage**
504
- *
505
- * ```bash
506
- * $ greet World
507
- * ```
508
- *
509
- * Output:
510
- * ```
511
- * Hello, World!
512
- * ```
513
- */
514
- function renderExamplesDefault(examples, results, opts) {
515
- if (examples.length === 0) return "";
516
- const showOutput = opts?.showOutput ?? true;
517
- const prefix = opts?.commandPrefix ? `${opts.commandPrefix} ` : "";
518
- const lines = [];
519
- for (let i = 0; i < examples.length; i++) {
520
- const example = examples[i];
521
- if (!example) continue;
522
- const result = results?.[i];
523
- lines.push(`**${example.desc}**`);
524
- lines.push("");
525
- lines.push("```bash");
526
- lines.push(`$ ${prefix}${example.cmd}`);
527
- if (showOutput) {
528
- if (result) {
529
- if (result.stdout) lines.push(result.stdout);
530
- if (result.stderr) lines.push(`[stderr] ${result.stderr}`);
531
- } else if (example.output) lines.push(example.output);
532
- }
533
- lines.push("```");
534
- lines.push("");
535
- }
536
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
537
- return lines.join("\n");
538
- }
539
- /**
540
- * Wrap content with section markers
541
- */
542
- function wrapWithMarker(type, scope, content) {
543
- return `${sectionStartMarker(type, scope)}\n${content}\n${sectionEndMarker(type, scope)}`;
544
- }
545
- /**
546
- * Generate a "See Global Options" link for subcommand documentation.
547
- * Returns null for root command or when no global options exist.
548
- */
549
- function getGlobalOptionsLink(info) {
550
- if (!info.hasGlobalOptions || info.commandPath === "") return null;
551
- return `See [Global Options](${info.rootDocPath && info.filePath && info.filePath !== info.rootDocPath ? `${getRelativePath(info.filePath, info.rootDocPath)}#global-options` : "#global-options"}) for options available to all commands.`;
552
- }
553
- function createCommandRenderer(options = {}) {
554
- const { headingLevel = 1, optionStyle = "table", generateAnchors = true, includeSubcommandDetails = true, renderDescription: customRenderDescription, renderUsage: customRenderUsage, renderArguments: customRenderArguments, renderOptions: customRenderOptions, renderSubcommands: customRenderSubcommands, renderNotes: customRenderNotes, renderFooter: customRenderFooter, renderExamples: customRenderExamples } = options;
555
- return (info) => {
556
- const sections = [];
557
- const scope = info.commandPath;
558
- const effectiveLevel = Math.min(headingLevel + (info.depth - 1), 6);
559
- const h = "#".repeat(effectiveLevel);
560
- const title = info.commandPath || info.name;
561
- sections.push(wrapWithMarker("heading", scope, `${h} ${title}`));
562
- {
563
- const parts = [];
564
- if (info.description) parts.push(info.description);
565
- if (info.aliases && info.aliases.length > 0) parts.push(`**Aliases:** ${info.aliases.map((a) => `\`${a}\``).join(", ")}`);
566
- if (parts.length > 0) {
567
- const context = {
568
- content: parts.join("\n\n"),
569
- heading: "",
570
- info
571
- };
572
- const content = customRenderDescription ? customRenderDescription(context) : context.content;
573
- if (content) sections.push(wrapWithMarker("description", scope, content));
574
- }
575
- }
576
- {
577
- const context = {
578
- content: `**Usage**\n\n\`\`\`\n${renderUsage(info)}\n\`\`\``,
579
- heading: "**Usage**",
580
- info
581
- };
582
- const content = customRenderUsage ? customRenderUsage(context) : context.content;
583
- if (content) sections.push(wrapWithMarker("usage", scope, content));
584
- }
585
- if (info.positionalArgs.length > 0) {
586
- const renderArgs = (args, opts) => {
587
- const style = opts?.style ?? optionStyle;
588
- const withHeading = opts?.withHeading ?? true;
589
- const content = style === "table" ? renderArgumentsTableFromArray(args) : renderArgumentsListFromArray(args);
590
- return withHeading ? `**Arguments**\n\n${content}` : content;
591
- };
592
- const context = {
593
- args: info.positionalArgs,
594
- render: renderArgs,
595
- heading: "**Arguments**",
596
- info
597
- };
598
- const content = customRenderArguments ? customRenderArguments(context) : renderArgs(context.args);
599
- if (content) sections.push(wrapWithMarker("arguments", scope, content));
600
- }
601
- if (info.options.length > 0) {
602
- const renderOpts = (opts, renderOpts) => {
603
- const style = renderOpts?.style ?? optionStyle;
604
- const withHeading = renderOpts?.withHeading ?? true;
605
- const extracted = info.extracted;
606
- let content;
607
- if (extracted && (extracted.schemaType === "union" || extracted.schemaType === "xor") && extracted.unionOptions) content = renderUnionOptionsMarkdown(extracted, style);
608
- else if (extracted && extracted.schemaType === "discriminatedUnion" && extracted.discriminator) content = renderDiscriminatedUnionOptionsMarkdown(extracted, style);
609
- else content = style === "table" ? renderOptionsTableFromArray(opts) : renderOptionsListFromArray(opts);
610
- return withHeading ? `**Options**\n\n${content}` : content;
611
- };
612
- const context = {
613
- options: info.options,
614
- render: renderOpts,
615
- heading: "**Options**",
616
- info
617
- };
618
- const content = customRenderOptions ? customRenderOptions(context) : renderOpts(context.options);
619
- if (content) sections.push(wrapWithMarker("options", scope, content));
620
- }
621
- {
622
- const globalLink = getGlobalOptionsLink(info);
623
- if (globalLink) sections.push(wrapWithMarker("global-options-link", scope, globalLink));
624
- }
625
- if (info.subCommands.length > 0) {
626
- const effectiveAnchors = generateAnchors && includeSubcommandDetails;
627
- const renderSubs = (subs, opts) => {
628
- const anchors = opts?.generateAnchors ?? effectiveAnchors;
629
- const withHeading = opts?.withHeading ?? true;
630
- const content = renderSubcommandsTableFromArray(subs, info, anchors);
631
- return withHeading ? `**Commands**\n\n${content}` : content;
632
- };
633
- const context = {
634
- subcommands: info.subCommands,
635
- render: renderSubs,
636
- heading: "**Commands**",
637
- info
638
- };
639
- const content = customRenderSubcommands ? customRenderSubcommands(context) : renderSubs(context.subcommands);
640
- if (content) sections.push(wrapWithMarker("subcommands", scope, content));
641
- }
642
- if (info.examples && info.examples.length > 0) {
643
- const renderEx = (examples, results, opts) => {
644
- const withHeading = opts?.withHeading ?? true;
645
- const content = renderExamplesDefault(examples, results, {
646
- commandPrefix: info.fullCommandPath,
647
- ...opts
648
- });
649
- return withHeading ? `**Examples**\n\n${content}` : content;
650
- };
651
- const context = {
652
- examples: info.examples,
653
- results: info.exampleResults,
654
- render: renderEx,
655
- heading: "**Examples**",
656
- info
657
- };
658
- const content = customRenderExamples ? customRenderExamples(context) : renderEx(context.examples, context.results);
659
- if (content) sections.push(wrapWithMarker("examples", scope, content));
660
- }
661
- if (info.notes) {
662
- const context = {
663
- content: `**Notes**\n\n${info.notes}`,
664
- heading: "**Notes**",
665
- info
666
- };
667
- const content = customRenderNotes ? customRenderNotes(context) : context.content;
668
- if (content) sections.push(wrapWithMarker("notes", scope, content));
669
- }
670
- {
671
- const context = {
672
- content: "",
673
- heading: "",
674
- info
675
- };
676
- const content = customRenderFooter ? customRenderFooter(context) : context.content;
677
- if (content) sections.push(content);
678
- }
679
- return sections.join("\n\n") + "\n";
680
- };
681
- }
682
- /**
683
- * Default renderers presets
684
- */
685
- const defaultRenderers = {
686
- /** Standard command documentation */
687
- command: (options) => createCommandRenderer(options),
688
- /** Table style options (default) */
689
- tableStyle: createCommandRenderer({ optionStyle: "table" }),
690
- /** List style options */
691
- listStyle: createCommandRenderer({ optionStyle: "list" })
692
- };
693
-
694
- //#endregion
695
- //#region src/docs/doc-comparator.ts
696
- /**
697
- * Compare generated content with existing file
698
- */
699
- function compareWithExisting(generatedContent, filePath) {
700
- const absolutePath = node_path.resolve(filePath);
701
- if (!node_fs.existsSync(absolutePath)) return {
702
- match: false,
703
- fileExists: false
704
- };
705
- const existingContent = node_fs.readFileSync(absolutePath, "utf-8");
706
- if (generatedContent === existingContent) return {
707
- match: true,
708
- fileExists: true
709
- };
710
- return {
711
- match: false,
712
- diff: formatDiff(existingContent, generatedContent),
713
- fileExists: true
714
- };
715
- }
716
- /**
717
- * Format diff between two strings in unified diff format
718
- */
719
- function formatDiff(expected, actual) {
720
- const expectedLines = expected.split("\n");
721
- const actualLines = actual.split("\n");
722
- const result = [];
723
- result.push("--- existing");
724
- result.push("+++ generated");
725
- result.push("");
726
- const maxLines = Math.max(expectedLines.length, actualLines.length);
727
- let inChunk = false;
728
- let chunkStart = 0;
729
- const chunk = [];
730
- const flushChunk = () => {
731
- if (chunk.length > 0) {
732
- result.push(`@@ -${chunkStart + 1},${chunk.length} @@`);
733
- result.push(...chunk);
734
- chunk.length = 0;
735
- }
736
- inChunk = false;
737
- };
738
- for (let i = 0; i < maxLines; i++) {
739
- const expectedLine = expectedLines[i];
740
- const actualLine = actualLines[i];
741
- if (expectedLine === actualLine) {
742
- if (inChunk) {
743
- chunk.push(` ${expectedLine ?? ""}`);
744
- const lastChangeIndex = chunk.findIndex((line, idx) => (line.startsWith("-") || line.startsWith("+")) && chunk.slice(idx + 1).every((l) => l.startsWith(" ")));
745
- if (lastChangeIndex !== -1 && chunk.length - lastChangeIndex > 3) flushChunk();
746
- }
747
- } else {
748
- if (!inChunk) {
749
- inChunk = true;
750
- chunkStart = i;
751
- const contextStart = Math.max(0, i - 3);
752
- for (let j = contextStart; j < i; j++) chunk.push(` ${expectedLines[j] ?? ""}`);
753
- }
754
- if (expectedLine !== void 0 && (actualLine === void 0 || expectedLine !== actualLine)) chunk.push(`-${expectedLine}`);
755
- if (actualLine !== void 0 && (expectedLine === void 0 || expectedLine !== actualLine)) chunk.push(`+${actualLine}`);
756
- }
757
- }
758
- flushChunk();
759
- return result.join("\n");
760
- }
761
- /**
762
- * Write content to file, creating directories if needed
763
- */
764
- function writeFile(filePath, content) {
765
- const absolutePath = node_path.resolve(filePath);
766
- const dir = node_path.dirname(absolutePath);
767
- if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, { recursive: true });
768
- node_fs.writeFileSync(absolutePath, content, "utf-8");
769
- }
770
- /**
771
- * Read file content if it exists
772
- * Returns null if file does not exist
773
- */
774
- function readFile(filePath) {
775
- const absolutePath = node_path.resolve(filePath);
776
- if (!node_fs.existsSync(absolutePath)) return null;
777
- return node_fs.readFileSync(absolutePath, "utf-8");
778
- }
779
- /**
780
- * Delete file if it exists
781
- * @param filePath - Path to the file to delete
782
- * @param fileSystem - Optional fs implementation (useful when fs is mocked)
783
- */
784
- function deleteFile(filePath, fileSystem = node_fs) {
785
- const absolutePath = node_path.resolve(filePath);
786
- if (fileSystem.existsSync(absolutePath)) fileSystem.unlinkSync(absolutePath);
787
- }
788
-
789
- //#endregion
790
- //#region src/docs/doc-generator.ts
791
- /**
792
- * Build CommandInfo from a command
793
- */
794
- async function buildCommandInfo(command, rootName, commandPath = []) {
795
- const extracted = require_schema_extractor.getExtractedFields(command);
796
- const positionalArgs = extracted?.fields.filter((f) => f.positional) ?? [];
797
- const options = extracted?.fields.filter((f) => !f.positional) ?? [];
798
- const subCommands = [];
799
- if (command.subCommands) for (const [name, subCmd] of Object.entries(command.subCommands)) {
800
- const resolved = await require_schema_extractor.resolveLazyCommand(subCmd);
801
- const fullPath = [...commandPath, name];
802
- subCommands.push({
803
- name,
804
- description: resolved.description,
805
- aliases: resolved.aliases,
806
- fullPath
807
- });
808
- }
809
- return {
810
- name: command.name ?? "",
811
- description: command.description,
812
- aliases: command.aliases,
813
- fullCommandPath: commandPath.length > 0 ? `${rootName} ${commandPath.join(" ")}` : rootName,
814
- commandPath: commandPath.join(" "),
815
- depth: commandPath.length + 1,
816
- positionalArgs,
817
- options,
818
- subCommands,
819
- extracted,
820
- command,
821
- notes: command.notes,
822
- examples: command.examples
823
- };
824
- }
825
- /**
826
- * Collect all commands with their paths
827
- * Returns a map of command path -> CommandInfo
828
- */
829
- async function collectAllCommands(command, rootName) {
830
- const root = rootName ?? command.name ?? "command";
831
- const result = /* @__PURE__ */ new Map();
832
- async function traverse(cmd, path) {
833
- const info = await buildCommandInfo(cmd, root, path);
834
- const pathKey = path.join(" ");
835
- result.set(pathKey, info);
836
- if (cmd.subCommands) for (const [name, subCmd] of Object.entries(cmd.subCommands)) await traverse(await require_schema_extractor.resolveLazyCommand(subCmd), [...path, name]);
837
- }
838
- await traverse(command, []);
839
- return result;
840
- }
841
-
842
- //#endregion
843
- //#region src/docs/example-executor.ts
844
- /**
845
- * Execute examples for a command and capture output
846
- *
847
- * @param examples - Examples to execute
848
- * @param config - Execution configuration (mock setup/cleanup)
849
- * @param rootCommand - Root command to execute against
850
- * @param commandPath - Command path for subcommands (e.g., ["config", "get"])
851
- * @returns Array of execution results with captured stdout/stderr
852
- */
853
- async function executeExamples(examples, config, rootCommand, commandPath = []) {
854
- const results = [];
855
- if (config.mock) await config.mock();
856
- try {
857
- for (const example of examples) {
858
- const result = await executeSingleExample(example, rootCommand, commandPath);
859
- results.push(result);
860
- }
861
- } finally {
862
- if (config.cleanup) await config.cleanup();
863
- }
864
- return results;
865
- }
866
- /**
867
- * Execute a single example and capture output
868
- */
869
- async function executeSingleExample(example, rootCommand, commandPath) {
870
- const exampleArgs = parseExampleCmd(example.cmd);
871
- const argv = [...commandPath, ...exampleArgs];
872
- const collector = require_log_collector.createLogCollector({ passthrough: false });
873
- collector.start();
874
- let success = true;
875
- try {
876
- const { runCommand } = await Promise.resolve().then(() => require("../runner-DvFvokV6.cjs")).then((n) => n.runner_exports);
877
- const result = await runCommand(rootCommand, argv);
878
- success = result.success;
879
- if (!result.success && result.error) console.error(result.error.message);
880
- } catch (error) {
881
- success = false;
882
- console.error(error instanceof Error ? error.message : String(error));
883
- } finally {
884
- collector.stop();
885
- }
886
- const logs = collector.getLogs();
887
- const stdout = logs.entries.filter((e) => e.stream === "stdout").map((e) => e.message).join("\n");
888
- const stderr = logs.entries.filter((e) => e.stream === "stderr").map((e) => e.message).join("\n");
889
- return {
890
- cmd: example.cmd,
891
- desc: example.desc,
892
- expectedOutput: example.output,
893
- stdout,
894
- stderr,
895
- success
896
- };
897
- }
898
- /**
899
- * Parse example command string into argv array
900
- * Handles quoted strings (single and double quotes)
901
- *
902
- * @example
903
- * parseExampleCmd('World') // ['World']
904
- * parseExampleCmd('--name "John Doe"') // ['--name', 'John Doe']
905
- * parseExampleCmd("--greeting 'Hello World'") // ['--greeting', 'Hello World']
906
- */
907
- function parseExampleCmd(cmd) {
908
- const args = [];
909
- let current = "";
910
- let inQuote = false;
911
- let quoteChar = "";
912
- for (let i = 0; i < cmd.length; i++) {
913
- const char = cmd[i];
914
- if ((char === "\"" || char === "'") && !inQuote) {
915
- inQuote = true;
916
- quoteChar = char;
917
- } else if (char === quoteChar && inQuote) {
918
- inQuote = false;
919
- quoteChar = "";
920
- } else if (char === " " && !inQuote) {
921
- if (current) {
922
- args.push(current);
923
- current = "";
924
- }
925
- } else current += char;
926
- }
927
- if (current) args.push(current);
928
- return args;
929
- }
930
-
931
- //#endregion
932
- //#region src/docs/render-args.ts
933
- /**
934
- * Extract ResolvedFieldMeta array from ArgsShape
935
- *
936
- * This converts a raw args shape (like `commonArgs`) into the
937
- * ResolvedFieldMeta format used by politty's rendering functions.
938
- */
939
- function extractArgsFields(args) {
940
- return require_schema_extractor.extractFields(zod.z.object(args)).fields;
941
- }
942
- /**
943
- * Render args definition as a markdown options table
944
- *
945
- * This function takes raw args definitions (like `commonArgs`) and
946
- * renders them as a markdown table suitable for documentation.
947
- *
948
- * @example
949
- * import { renderArgsTable } from "politty/docs";
950
- * import { commonArgs, workspaceArgs } from "./args";
951
- *
952
- * const table = renderArgsTable({
953
- * ...commonArgs,
954
- * ...workspaceArgs,
955
- * });
956
- * // | Option | Alias | Description | Default |
957
- * // |--------|-------|-------------|---------|
958
- * // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
959
- * // ...
960
- *
961
- * @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
962
- * @param options - Rendering options
963
- * @returns Rendered markdown table string
964
- */
965
- function renderArgsTable(args, options) {
966
- const optionFields = extractArgsFields(args).filter((f) => !f.positional);
967
- if (optionFields.length === 0) return "";
968
- if (options?.columns) return renderFilteredTable(optionFields, options.columns);
969
- return renderOptionsTableFromArray(optionFields);
970
- }
971
- /**
972
- * Escape markdown special characters in table cells
973
- */
974
- function escapeTableCell$1(str) {
975
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
976
- }
977
- /**
978
- * Format default value for display
979
- */
980
- function formatDefaultValue(value) {
981
- if (value === void 0) return "-";
982
- return `\`${JSON.stringify(value)}\``;
983
- }
984
- /**
985
- * Render table with filtered columns
986
- */
987
- function renderFilteredTable(options, columns) {
988
- const lines = [];
989
- const headerCells = [];
990
- const separatorCells = [];
991
- for (const col of columns) switch (col) {
992
- case "option":
993
- headerCells.push("Option");
994
- separatorCells.push("------");
995
- break;
996
- case "alias":
997
- headerCells.push("Alias");
998
- separatorCells.push("-----");
999
- break;
1000
- case "description":
1001
- headerCells.push("Description");
1002
- separatorCells.push("-----------");
1003
- break;
1004
- case "required":
1005
- headerCells.push("Required");
1006
- separatorCells.push("--------");
1007
- break;
1008
- case "default":
1009
- headerCells.push("Default");
1010
- separatorCells.push("-------");
1011
- break;
1012
- case "env":
1013
- headerCells.push("Env");
1014
- separatorCells.push("---");
1015
- break;
1016
- }
1017
- lines.push(`| ${headerCells.join(" | ")} |`);
1018
- lines.push(`| ${separatorCells.join(" | ")} |`);
1019
- for (const opt of options) {
1020
- const cells = [];
1021
- for (const col of columns) switch (col) {
1022
- case "option": {
1023
- const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
1024
- let optionName;
1025
- if (opt.type === "boolean") {
1026
- optionName = `\`--${opt.cliName}\``;
1027
- if (opt.negationDisplay && !opt.negationDescription) optionName += ` / \`--${opt.negationDisplay}\``;
1028
- } else optionName = `\`--${opt.cliName} <${placeholder}>\``;
1029
- cells.push(optionName);
1030
- break;
1031
- }
1032
- case "alias":
1033
- cells.push(opt.alias && opt.alias.length > 0 ? opt.alias.map((a) => `\`${a.length === 1 ? `-${a}` : `--${a}`}\``).join(", ") : "-");
1034
- break;
1035
- case "description":
1036
- cells.push(escapeTableCell$1(opt.description ?? ""));
1037
- break;
1038
- case "required":
1039
- cells.push(opt.required ? "Yes" : "No");
1040
- break;
1041
- case "default":
1042
- cells.push(formatDefaultValue(opt.defaultValue));
1043
- break;
1044
- case "env": {
1045
- const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
1046
- cells.push(envNames);
1047
- break;
1048
- }
1049
- }
1050
- lines.push(`| ${cells.join(" | ")} |`);
1051
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
1052
- const negCells = [];
1053
- for (const col of columns) switch (col) {
1054
- case "option":
1055
- negCells.push(`\`--${opt.negationDisplay}\``);
1056
- break;
1057
- case "description":
1058
- negCells.push(`${escapeTableCell$1(opt.negationDescription)} ${negationRelationMarker(opt)}`);
1059
- break;
1060
- case "required":
1061
- negCells.push(opt.required ? "Yes" : "No");
1062
- break;
1063
- case "alias":
1064
- case "default":
1065
- case "env":
1066
- negCells.push("-");
1067
- break;
1068
- }
1069
- lines.push(`| ${negCells.join(" | ")} |`);
1070
- }
1071
- }
1072
- return lines.join("\n");
1073
- }
1074
-
1075
- //#endregion
1076
- //#region src/docs/render-index.ts
1077
- /**
1078
- * Escape markdown special characters in table cells
1079
- */
1080
- function escapeTableCell(str) {
1081
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
1082
- }
1083
- /**
1084
- * Generate anchor from command path
1085
- */
1086
- function generateAnchor(commandPath) {
1087
- return commandPath.replace(/\s+/g, "-").toLowerCase();
1088
- }
1089
- /**
1090
- * Check if a command is a leaf (has no subcommands)
1091
- */
1092
- function isLeafCommand(info) {
1093
- return info.subCommands.length === 0;
1094
- }
1095
- /**
1096
- * Expand commands to include their subcommands
1097
- * If a command has subcommands, recursively find all commands under it
1098
- *
1099
- * @param commandPaths - Command paths to expand
1100
- * @param allCommands - Map of all available commands
1101
- * @param leafOnly - If true, only include leaf commands; if false, include all commands
1102
- */
1103
- function expandCommands(commandPaths, allCommands, leafOnly) {
1104
- const result = [];
1105
- for (const cmdPath of commandPaths) {
1106
- const info = allCommands.get(cmdPath);
1107
- if (!info) continue;
1108
- if (isLeafCommand(info)) result.push(cmdPath);
1109
- else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
1110
- if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
1111
- }
1112
- }
1113
- return result;
1114
- }
1115
- /**
1116
- * Render a single category section
1117
- */
1118
- function renderCategory(category, allCommands, headingLevel, leafOnly) {
1119
- const h = "#".repeat(headingLevel);
1120
- const lines = [];
1121
- lines.push(`${h} [${category.title}](${category.docPath})`);
1122
- lines.push("");
1123
- lines.push(category.description);
1124
- lines.push("");
1125
- const commandPaths = expandCommands(category.commands, allCommands, leafOnly);
1126
- lines.push("| Command | Description |");
1127
- lines.push("|---------|-------------|");
1128
- for (const cmdPath of commandPaths) {
1129
- const info = allCommands.get(cmdPath);
1130
- if (!info) continue;
1131
- if (leafOnly && !isLeafCommand(info)) continue;
1132
- const displayName = cmdPath || info.name;
1133
- const anchor = generateAnchor(displayName);
1134
- const desc = escapeTableCell(info.description ?? "");
1135
- lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
1136
- }
1137
- return lines.join("\n");
1138
- }
1139
- /**
1140
- * Render command index from categories
1141
- *
1142
- * Generates a category-based index of commands with links to documentation.
1143
- *
1144
- * @example
1145
- * const categories: CommandCategory[] = [
1146
- * {
1147
- * title: "Application Commands",
1148
- * description: "Commands for managing applications.",
1149
- * commands: ["init", "generate", "apply"],
1150
- * docPath: "./cli/application.md",
1151
- * },
1152
- * ];
1153
- *
1154
- * const index = await renderCommandIndex(mainCommand, categories);
1155
- * // ### [Application Commands](./cli/application.md)
1156
- * //
1157
- * // Commands for managing applications.
1158
- * //
1159
- * // | Command | Description |
1160
- * // |---------|-------------|
1161
- * // | [init](./cli/application.md#init) | Initialize a project |
1162
- * // ...
1163
- *
1164
- * @param command - Root command to extract command information from
1165
- * @param categories - Category definitions for grouping commands
1166
- * @param options - Rendering options
1167
- * @returns Rendered markdown string
1168
- */
1169
- async function renderCommandIndex(command, categories, options) {
1170
- const headingLevel = options?.headingLevel ?? 3;
1171
- const leafOnly = options?.leafOnly ?? true;
1172
- const allCommands = await collectAllCommands(command);
1173
- const sections = [];
1174
- for (const category of categories) {
1175
- const section = renderCategory(category, allCommands, headingLevel, leafOnly);
1176
- sections.push(section);
1177
- }
1178
- return sections.join("\n\n");
1179
- }
1180
-
1181
- //#endregion
1182
- //#region src/docs/golden-test.ts
1183
- /**
1184
- * Apply formatter to content if provided
1185
- * Supports both sync and async formatters
1186
- */
1187
- async function applyFormatter(content, formatter) {
1188
- if (!formatter) return content;
1189
- const formatted = await formatter(content);
1190
- if (!content.endsWith("\n") && formatted.endsWith("\n")) return formatted.slice(0, -1);
1191
- return formatted;
1192
- }
1193
- function isTruthyEnv(envKey) {
1194
- const value = process.env[envKey];
1195
- return value === "true" || value === "1";
1196
- }
1197
- /**
1198
- * Normalize file mapping entry to FileConfig
1199
- */
1200
- function normalizeFileConfig(config) {
1201
- if (Array.isArray(config)) return { commands: config };
1202
- if (!("commands" in config) || !Array.isArray(config.commands)) throw new Error("Invalid file config: object form must include a \"commands\" array. Use [] to skip generation intentionally.");
1203
- return config;
1204
- }
1205
- /**
1206
- * Check if a command path is a subcommand of another
1207
- */
1208
- function isSubcommandOf(childPath, parentPath) {
1209
- if (parentPath === "") return true;
1210
- if (childPath === parentPath) return true;
1211
- return childPath.startsWith(parentPath + " ");
1212
- }
1213
- /**
1214
- * Check if a pattern contains wildcards
1215
- */
1216
- function containsWildcard(pattern) {
1217
- return pattern.includes("*");
1218
- }
1219
- /**
1220
- * Check if a command path matches a wildcard pattern
1221
- * - `*` matches any single command segment
1222
- * - Pattern segments are space-separated
1223
- *
1224
- * @example
1225
- * matchesWildcard("config get", "* *") // true
1226
- * matchesWildcard("config", "* *") // false
1227
- * matchesWildcard("config get", "config *") // true
1228
- * matchesWildcard("greet", "*") // true
1229
- */
1230
- function matchesWildcard(path, pattern) {
1231
- const pathSegments = path === "" ? [] : path.split(" ");
1232
- const patternSegments = pattern === "" ? [] : pattern.split(" ");
1233
- if (pathSegments.length !== patternSegments.length) return false;
1234
- for (let i = 0; i < patternSegments.length; i++) {
1235
- const patternSeg = patternSegments[i];
1236
- const pathSeg = pathSegments[i];
1237
- if (patternSeg !== "*" && patternSeg !== pathSeg) return false;
1238
- }
1239
- return true;
1240
- }
1241
- /**
1242
- * Expand a wildcard pattern to matching command paths
1243
- */
1244
- function expandWildcardPattern(pattern, allCommands) {
1245
- const matches = [];
1246
- for (const cmdPath of allCommands.keys()) if (matchesWildcard(cmdPath, pattern)) matches.push(cmdPath);
1247
- return matches;
1248
- }
1249
- /**
1250
- * Check if a path matches any ignore pattern (with wildcard support)
1251
- * For wildcard patterns, also ignores subcommands of matched commands
1252
- */
1253
- function matchesIgnorePattern(path, ignorePattern) {
1254
- if (containsWildcard(ignorePattern)) {
1255
- if (matchesWildcard(path, ignorePattern)) return true;
1256
- const pathSegments = path === "" ? [] : path.split(" ");
1257
- const patternSegments = ignorePattern === "" ? [] : ignorePattern.split(" ");
1258
- if (pathSegments.length > patternSegments.length) return matchesWildcard(pathSegments.slice(0, patternSegments.length).join(" "), ignorePattern);
1259
- return false;
1260
- }
1261
- return isSubcommandOf(path, ignorePattern);
1262
- }
1263
- /**
1264
- * Expand command paths to include all subcommands (with wildcard support)
1265
- */
1266
- function expandCommandPaths(commandPaths, allCommands) {
1267
- const expanded = /* @__PURE__ */ new Set();
1268
- const resolved = commandPaths.flatMap((cmdPath) => containsWildcard(cmdPath) ? expandWildcardPattern(cmdPath, allCommands) : [cmdPath]);
1269
- for (const cmdPath of resolved) for (const existingPath of allCommands.keys()) if (isSubcommandOf(existingPath, cmdPath)) expanded.add(existingPath);
1270
- return Array.from(expanded);
1271
- }
1272
- /**
1273
- * Filter out ignored commands (with wildcard support)
1274
- */
1275
- function filterIgnoredCommands(commandPaths, ignores) {
1276
- return commandPaths.filter((path) => {
1277
- return !ignores.some((ignorePattern) => matchesIgnorePattern(path, ignorePattern));
1278
- });
1279
- }
1280
- /**
1281
- * Resolve wildcards to direct matches without subcommand expansion.
1282
- * Returns the "top-level" commands for use in CommandCategory.commands,
1283
- * where expandCommands in render-index handles subcommand expansion.
1284
- */
1285
- function resolveTopLevelCommands(specifiedCommands, allCommands) {
1286
- const result = [];
1287
- for (const cmdPath of specifiedCommands) if (containsWildcard(cmdPath)) result.push(...expandWildcardPattern(cmdPath, allCommands));
1288
- else if (allCommands.has(cmdPath)) result.push(cmdPath);
1289
- return result;
1290
- }
1291
- /**
1292
- * Resolve file command configuration to concrete command paths.
1293
- * This applies wildcard/subcommand expansion and ignore filtering.
1294
- */
1295
- function resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores) {
1296
- const fileConfig = normalizeFileConfig(fileConfigRaw);
1297
- const specifiedCommands = fileConfig.commands;
1298
- return {
1299
- fileConfig,
1300
- specifiedCommands,
1301
- commandPaths: filterIgnoredCommands(fileConfig.noExpand ? specifiedCommands.filter((p) => allCommands.has(p)) : expandCommandPaths(specifiedCommands, allCommands), ignores),
1302
- topLevelCommands: filterIgnoredCommands(resolveTopLevelCommands(specifiedCommands, allCommands), ignores)
1303
- };
1304
- }
1305
- /**
1306
- * Validate that there are no conflicts between files and ignores (with wildcard support)
1307
- */
1308
- function validateNoConflicts(filesCommands, ignores, allCommands) {
1309
- const conflicts = [];
1310
- for (const filePattern of filesCommands) {
1311
- const filePaths = containsWildcard(filePattern) ? expandWildcardPattern(filePattern, allCommands) : [filePattern];
1312
- for (const filePath of filePaths) for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
1313
- if (matchesWildcard(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
1314
- } else if (filePath === ignorePattern || isSubcommandOf(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
1315
- }
1316
- if (conflicts.length > 0) throw new Error(`Conflict between files and ignores:\n - ${conflicts.join("\n - ")}`);
1317
- }
1318
- /**
1319
- * Validate that all ignored paths exist in the command tree (with wildcard support)
1320
- */
1321
- function validateIgnoresExist(ignores, allCommands) {
1322
- const nonExistent = [];
1323
- for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
1324
- if (expandWildcardPattern(ignorePattern, allCommands).length === 0) nonExistent.push(`"${ignorePattern}"`);
1325
- } else if (!allCommands.has(ignorePattern)) nonExistent.push(`"${ignorePattern}"`);
1326
- if (nonExistent.length > 0) throw new Error(`Ignored command paths do not exist: ${nonExistent.join(", ")}`);
1327
- }
1328
- /**
1329
- * Sort command paths in depth-first order while preserving the specified command order
1330
- * Parent commands are immediately followed by their subcommands
1331
- */
1332
- function sortDepthFirst(commandPaths, specifiedOrder) {
1333
- const pathSet = new Set(commandPaths);
1334
- const topLevelPaths = specifiedOrder.filter((cmd) => pathSet.has(cmd));
1335
- for (const path of commandPaths) if ((path === "" ? 0 : path.split(" ").length) === 1 && !topLevelPaths.includes(path)) topLevelPaths.push(path);
1336
- const result = [];
1337
- const visited = /* @__PURE__ */ new Set();
1338
- function addWithChildren(cmdPath) {
1339
- if (visited.has(cmdPath) || !pathSet.has(cmdPath)) return;
1340
- visited.add(cmdPath);
1341
- result.push(cmdPath);
1342
- const children = commandPaths.filter((p) => {
1343
- if (p === cmdPath || visited.has(p)) return false;
1344
- if (cmdPath === "") return p.split(" ").length === 1;
1345
- return p.startsWith(cmdPath + " ") && p.split(" ").length === cmdPath.split(" ").length + 1;
1346
- }).sort((a, b) => a.localeCompare(b));
1347
- for (const child of children) addWithChildren(child);
1348
- }
1349
- for (const topLevel of topLevelPaths) addWithChildren(topLevel);
1350
- for (const path of commandPaths) if (!visited.has(path)) result.push(path);
1351
- return result;
1352
- }
1353
- function generateFileHeader(fileConfig) {
1354
- if (!fileConfig.title && !fileConfig.description) return null;
1355
- const parts = [];
1356
- if (fileConfig.title) {
1357
- const heading = "#".repeat(fileConfig.headingLevel ?? 1);
1358
- parts.push(`${heading} ${fileConfig.title}`);
1359
- }
1360
- if (fileConfig.description) {
1361
- parts.push("");
1362
- parts.push(fileConfig.description);
1363
- }
1364
- parts.push("");
1365
- return parts.join("\n");
1366
- }
1367
- /**
1368
- * Extract a leading file header (title and optional description paragraph)
1369
- */
1370
- function extractFileHeader(content) {
1371
- if (!/^#{1,6} /.test(content)) return null;
1372
- const titleEnd = content.indexOf("\n");
1373
- if (titleEnd === -1) return content;
1374
- let cursor = titleEnd + 1;
1375
- if (content[cursor] === "\n") cursor += 1;
1376
- while (cursor < content.length) {
1377
- const lineEnd = content.indexOf("\n", cursor);
1378
- const line = lineEnd === -1 ? content.slice(cursor) : content.slice(cursor, lineEnd);
1379
- if (line.length === 0 || /^#{1,6}\s/.test(line) || line.startsWith("<!-- politty:")) break;
1380
- cursor = lineEnd === -1 ? content.length : lineEnd + 1;
1381
- }
1382
- return content.slice(0, cursor);
1383
- }
1384
- /**
1385
- * Validate and optionally update configured file header
1386
- */
1387
- function processFileHeader(existingContent, fileConfig, updateMode) {
1388
- const generatedHeader = generateFileHeader(fileConfig);
1389
- if (!generatedHeader) return {
1390
- content: existingContent,
1391
- hasError: false,
1392
- wasUpdated: false
1393
- };
1394
- if (existingContent.startsWith(generatedHeader)) return {
1395
- content: existingContent,
1396
- hasError: false,
1397
- wasUpdated: false
1398
- };
1399
- const existingHeader = extractFileHeader(existingContent) ?? "";
1400
- if (!updateMode) return {
1401
- content: existingContent,
1402
- diff: formatDiff(existingHeader, generatedHeader),
1403
- hasError: true,
1404
- wasUpdated: false
1405
- };
1406
- return {
1407
- content: `${generatedHeader}${(existingHeader ? existingContent.slice(existingHeader.length) : existingContent).replace(/^\n+/, "")}`,
1408
- hasError: false,
1409
- wasUpdated: true
1410
- };
1411
- }
1412
- function formatCommandPath(commandPath) {
1413
- return commandPath === "" ? "<root>" : commandPath;
1414
- }
1415
- /**
1416
- * Extract a section marker's content from document content.
1417
- * Returns the content between start and end markers (including markers).
1418
- */
1419
- function extractSectionMarker(content, type, scope) {
1420
- return extractMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope));
1421
- }
1422
- /**
1423
- * Replace a section marker's content in document content.
1424
- * Returns updated content, or null if marker not found.
1425
- */
1426
- function replaceSectionMarker(content, type, scope, newContent) {
1427
- return replaceMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope), newContent);
1428
- }
1429
- /**
1430
- * Insert a new section marker into existing content at the correct position
1431
- * relative to other section markers for the same command, based on SECTION_TYPES order.
1432
- * Preserves any existing content between adjacent markers by wrapping it with the new markers
1433
- * instead of replacing it with generated content.
1434
- * @throws If no adjacent marker is found (unreachable when at least one marker exists for the command)
1435
- */
1436
- function insertSectionMarkerAtOrder(content, type, scope, generatedSection) {
1437
- const typeIndex = SECTION_TYPES.indexOf(type);
1438
- const startMarker = sectionStartMarker(type, scope);
1439
- const endMarker = sectionEndMarker(type, scope);
1440
- let prevBoundary = null;
1441
- for (let i = typeIndex - 1; i >= 0; i--) {
1442
- const prevType = SECTION_TYPES[i];
1443
- const prevEnd = sectionEndMarker(prevType, scope);
1444
- const prevEndIdx = content.indexOf(prevEnd);
1445
- if (prevEndIdx !== -1) {
1446
- prevBoundary = prevEndIdx + prevEnd.length;
1447
- break;
1448
- }
1449
- }
1450
- let nextBoundary = null;
1451
- for (let i = typeIndex + 1; i < SECTION_TYPES.length; i++) {
1452
- const nextType = SECTION_TYPES[i];
1453
- const nextStart = sectionStartMarker(nextType, scope);
1454
- const nextStartIdx = content.indexOf(nextStart);
1455
- if (nextStartIdx !== -1) {
1456
- nextBoundary = nextStartIdx;
1457
- break;
1458
- }
1459
- }
1460
- if (prevBoundary != null && nextBoundary != null) {
1461
- const wrapped = startMarker + content.slice(prevBoundary, nextBoundary).replace(/^\n+/, "\n").replace(/\n+$/, "\n") + endMarker;
1462
- return content.slice(0, prevBoundary) + "\n\n" + wrapped + "\n\n" + content.slice(nextBoundary);
1463
- }
1464
- if (prevBoundary != null) {
1465
- let afterPos = prevBoundary;
1466
- while (afterPos < content.length && content[afterPos] === "\n") afterPos++;
1467
- return content.slice(0, prevBoundary) + "\n\n" + generatedSection + (afterPos < content.length ? "\n\n" : "\n") + content.slice(afterPos);
1468
- }
1469
- if (nextBoundary != null) {
1470
- let beforePos = nextBoundary;
1471
- while (beforePos > 0 && content[beforePos - 1] === "\n") beforePos--;
1472
- const prefix = beforePos === 0 ? "" : "\n\n";
1473
- return content.slice(0, beforePos) + prefix + generatedSection + "\n\n" + content.slice(nextBoundary);
1474
- }
1475
- throw new Error(`No insertion point found for section "${type}" (scope="${scope}"). This should be unreachable when at least one marker exists for the command.`);
1476
- }
1477
- /**
1478
- * Collect all section types that have markers for a given command path.
1479
- */
1480
- function collectSectionMarkers(content, commandPath) {
1481
- const found = [];
1482
- for (const type of SECTION_TYPES) if (extractSectionMarker(content, type, commandPath) !== null) found.push(type);
1483
- return found;
1484
- }
1485
- /**
1486
- * Collect all command paths that have any section markers in the content.
1487
- */
1488
- function collectSectionMarkerPaths(content) {
1489
- const sectionTypes = SECTION_TYPES.join("|");
1490
- const markerPattern = new RegExp(`<!--\\s*politty:command:(.*?):(?:${sectionTypes}):start\\s*-->`, "g");
1491
- const paths = /* @__PURE__ */ new Set();
1492
- for (const match of content.matchAll(markerPattern)) paths.add(match[1] ?? "");
1493
- return Array.from(paths);
1494
- }
1495
- /**
1496
- * Insert command section markers at the correct position based on specified order.
1497
- * Uses the heading marker of adjacent commands as reference points.
1498
- */
1499
- function insertCommandSections(content, commandPath, newSection, specifiedOrder) {
1500
- const targetIndex = specifiedOrder.indexOf(commandPath);
1501
- if (targetIndex === -1) return content.trimEnd() + "\n\n" + newSection + "\n";
1502
- for (let i = targetIndex + 1; i < specifiedOrder.length; i++) {
1503
- const nextCmd = specifiedOrder[i];
1504
- if (nextCmd === void 0) continue;
1505
- const nextMarker = sectionStartMarker("heading", nextCmd);
1506
- const nextIndex = content.indexOf(nextMarker);
1507
- if (nextIndex !== -1) {
1508
- let insertPos = nextIndex;
1509
- while (insertPos > 0 && content[insertPos - 1] === "\n") insertPos--;
1510
- if (insertPos < nextIndex) insertPos++;
1511
- return content.slice(0, insertPos) + newSection + "\n" + content.slice(nextIndex);
1512
- }
1513
- }
1514
- for (let i = targetIndex - 1; i >= 0; i--) {
1515
- const prevCmd = specifiedOrder[i];
1516
- if (prevCmd === void 0) continue;
1517
- const prevMarkers = collectSectionMarkers(content, prevCmd);
1518
- if (prevMarkers.length > 0) {
1519
- const lastType = prevMarkers[prevMarkers.length - 1];
1520
- const prevEndMarker = sectionEndMarker(lastType, prevCmd);
1521
- const prevEndIndex = content.indexOf(prevEndMarker);
1522
- if (prevEndIndex !== -1) {
1523
- const insertPos = prevEndIndex + prevEndMarker.length;
1524
- return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
1525
- }
1526
- }
1527
- }
1528
- return content.trimEnd() + "\n" + newSection + "\n";
1529
- }
1530
- /**
1531
- * Remove all section markers for a command from content.
1532
- * Returns the content with all markers for the command removed and excess blank lines cleaned up.
1533
- */
1534
- function removeCommandSections(content, commandPath) {
1535
- const markers = collectSectionMarkers(content, commandPath);
1536
- for (const type of markers) {
1537
- const start = sectionStartMarker(type, commandPath);
1538
- const end = sectionEndMarker(type, commandPath);
1539
- let startIndex = content.indexOf(start);
1540
- while (startIndex !== -1) {
1541
- const endIndex = content.indexOf(end, startIndex);
1542
- if (endIndex === -1) break;
1543
- content = content.slice(0, startIndex) + content.slice(endIndex + end.length);
1544
- startIndex = content.indexOf(start, startIndex);
1545
- }
1546
- }
1547
- content = content.replace(/\n{3,}/g, "\n\n");
1548
- return content;
1549
- }
1550
- /**
1551
- * Extract a marker section from content
1552
- * Returns the content between start and end markers (including markers)
1553
- */
1554
- function extractMarkerSection(content, startMarker, endMarker) {
1555
- const startIndex = content.indexOf(startMarker);
1556
- if (startIndex === -1) return null;
1557
- const endIndex = content.indexOf(endMarker, startIndex);
1558
- if (endIndex === -1) return null;
1559
- return content.slice(startIndex, endIndex + endMarker.length);
1560
- }
1561
- /**
1562
- * Replace a marker section in content
1563
- * Returns the updated content with the new section
1564
- */
1565
- function replaceMarkerSection(content, startMarker, endMarker, newSection) {
1566
- const startIndex = content.indexOf(startMarker);
1567
- if (startIndex === -1) return null;
1568
- const endIndex = content.indexOf(endMarker, startIndex);
1569
- if (endIndex === -1) return null;
1570
- return content.slice(0, startIndex) + newSection + content.slice(endIndex + endMarker.length);
1571
- }
1572
- /**
1573
- * Check if config is the { args, options? } shape (not shorthand ArgsShape)
1574
- *
1575
- * Distinguishes between:
1576
- * - { args: ArgsShape, options?: ArgsTableOptions } → returns true
1577
- * - ArgsShape (e.g., { verbose: ZodType, args: ZodType }) → returns false
1578
- *
1579
- * The key insight is that in the { args, options? } shape, config.args is an ArgsShape
1580
- * (Record of ZodTypes), while in shorthand, config itself is the ArgsShape and config.args
1581
- * would be a single ZodType if user has an option named "args".
1582
- */
1583
- function isGlobalOptionsConfigWithOptions(config) {
1584
- if (typeof config !== "object" || config === null || !("args" in config)) return false;
1585
- return !(config.args instanceof zod.z.ZodType);
1586
- }
1587
- /**
1588
- * Collect option fields that are actually rendered by global options markers.
1589
- * Positional args are not rendered in args tables, so they must not be excluded.
1590
- */
1591
- function collectRenderableGlobalOptionFields(argsShape) {
1592
- return require_schema_extractor.extractFields(zod.z.object(argsShape)).fields.filter((field) => !field.positional);
1593
- }
1594
- /**
1595
- * Compare option definitions for global-options compatibility.
1596
- */
1597
- function areGlobalOptionsEquivalent(a, b) {
1598
- const { schema: _aSchema, ...aRest } = a;
1599
- const { schema: _bSchema, ...bRest } = b;
1600
- return (0, node_util.isDeepStrictEqual)(aRest, bRest);
1601
- }
1602
- /**
1603
- * Normalize rootDoc.globalOptions to { args, options? } form.
1604
- */
1605
- function normalizeGlobalOptions(config) {
1606
- if (!config) return void 0;
1607
- return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
1608
- }
1609
- /**
1610
- * Collect global option definitions from rootDoc.
1611
- * Global options are intentionally applied to all generated command sections.
1612
- */
1613
- function collectGlobalOptionDefinitions(rootDoc) {
1614
- const globalOptions = /* @__PURE__ */ new Map();
1615
- if (!rootDoc?.globalOptions) return globalOptions;
1616
- const normalized = normalizeGlobalOptions(rootDoc.globalOptions);
1617
- if (!normalized) return globalOptions;
1618
- for (const field of collectRenderableGlobalOptionFields(normalized.args)) globalOptions.set(field.name, field);
1619
- return globalOptions;
1620
- }
1621
- /**
1622
- * Derive CommandCategory[] from files mapping.
1623
- * Category title/description come from the first command in each file entry.
1624
- */
1625
- function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
1626
- const categories = [];
1627
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1628
- const { commandPaths, topLevelCommands } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1629
- if (commandPaths.length === 0) continue;
1630
- const docPath = "./" + node_path.relative(node_path.dirname(rootDocPath), filePath).replace(/\\/g, "/");
1631
- const firstCmdPath = commandPaths[0];
1632
- const cmdInfo = firstCmdPath !== void 0 ? allCommands.get(firstCmdPath) : void 0;
1633
- const fileConfig = Array.isArray(fileConfigRaw) ? void 0 : fileConfigRaw;
1634
- categories.push({
1635
- title: fileConfig?.title ?? cmdInfo?.name ?? node_path.basename(filePath, node_path.extname(filePath)),
1636
- description: fileConfig?.description ?? cmdInfo?.description ?? "",
1637
- commands: topLevelCommands,
1638
- docPath
1639
- });
1640
- }
1641
- return categories;
1642
- }
1643
- /**
1644
- * Collect command paths that are actually documented in configured files.
1645
- */
1646
- function collectDocumentedCommandPaths(files, allCommands, ignores) {
1647
- const documentedCommandPaths = /* @__PURE__ */ new Set();
1648
- for (const fileConfigRaw of Object.values(files)) {
1649
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1650
- for (const commandPath of commandPaths) documentedCommandPaths.add(commandPath);
1651
- }
1652
- return documentedCommandPaths;
1653
- }
1654
- /**
1655
- * Collect command paths that are targeted in configured files.
1656
- */
1657
- function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) {
1658
- const documentedTargetCommandPaths = /* @__PURE__ */ new Set();
1659
- for (const filePath of Object.keys(files)) {
1660
- const targetCommandsInFile = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
1661
- for (const commandPath of targetCommandsInFile) documentedTargetCommandPaths.add(commandPath);
1662
- }
1663
- return documentedTargetCommandPaths;
1664
- }
1665
- /**
1666
- * Validate that excluded command options match globalOptions definitions.
1667
- */
1668
- function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptions) {
1669
- if (globalOptions.size === 0) return;
1670
- const conflicts = [];
1671
- for (const commandPath of documentedCommandPaths) {
1672
- const info = allCommands.get(commandPath);
1673
- if (!info) continue;
1674
- for (const option of info.options) {
1675
- const globalOption = globalOptions.get(option.name);
1676
- if (!globalOption) continue;
1677
- if (!areGlobalOptionsEquivalent(globalOption, option)) conflicts.push(`Command "${formatCommandPath(commandPath)}" option "--${option.cliName}" does not match globalOptions definition for "${option.name}".`);
1678
- }
1679
- }
1680
- if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
1681
- }
1682
- /**
1683
- * Generate global options section content with markers
1684
- */
1685
- function generateGlobalOptionsSection(config) {
1686
- const startMarker = globalOptionsStartMarker();
1687
- const endMarker = globalOptionsEndMarker();
1688
- return [
1689
- startMarker,
1690
- "<a id=\"global-options\"></a>",
1691
- renderArgsTable(config.args, config.options),
1692
- endMarker
1693
- ].join("\n");
1694
- }
1695
- /**
1696
- * Generate index section content with markers
1697
- */
1698
- async function generateIndexSection(categories, command, scope, options) {
1699
- const startMarker = indexStartMarker(scope);
1700
- const endMarker = indexEndMarker(scope);
1701
- return [
1702
- startMarker,
1703
- await renderCommandIndex(command, categories, options),
1704
- endMarker
1705
- ].join("\n");
1706
- }
1707
- /**
1708
- * Normalize a doc file path for equivalence checks.
1709
- */
1710
- function normalizeDocPathForComparison(filePath) {
1711
- return node_path.resolve(filePath);
1712
- }
1713
- /**
1714
- * Process global options marker in file content
1715
- * Returns result with updated content and any diffs
1716
- */
1717
- async function processGlobalOptionsMarker(existingContent, globalOptionsConfig, updateMode, formatter, autoInsertIfMissing) {
1718
- let content = existingContent;
1719
- const diffs = [];
1720
- let hasError = false;
1721
- let wasUpdated = false;
1722
- const startMarker = globalOptionsStartMarker();
1723
- const endMarker = globalOptionsEndMarker();
1724
- const generatedSection = await applyFormatter(generateGlobalOptionsSection(globalOptionsConfig), formatter);
1725
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
1726
- if (!existingSection) {
1727
- if (updateMode && autoInsertIfMissing) {
1728
- content = content.trimEnd() + "\n\n" + generatedSection + "\n";
1729
- wasUpdated = true;
1730
- return {
1731
- content,
1732
- diffs,
1733
- hasError,
1734
- wasUpdated
1735
- };
1736
- }
1737
- hasError = true;
1738
- diffs.push(`Global options marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
1739
- return {
1740
- content,
1741
- diffs,
1742
- hasError,
1743
- wasUpdated
1744
- };
1745
- }
1746
- if (existingSection !== generatedSection) if (updateMode) {
1747
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
1748
- if (updated) {
1749
- content = updated;
1750
- wasUpdated = true;
1751
- } else {
1752
- hasError = true;
1753
- diffs.push("Failed to replace global options section");
1754
- }
1755
- } else {
1756
- hasError = true;
1757
- diffs.push(formatDiff(existingSection, generatedSection));
1758
- }
1759
- return {
1760
- content,
1761
- diffs,
1762
- hasError,
1763
- wasUpdated
1764
- };
1765
- }
1766
- /**
1767
- * Process a static content marker (root-header or root-footer).
1768
- * Inserts/updates the marker section with the given content.
1769
- */
1770
- async function processStaticMarker(existingContent, markerLabel, startMarker, endMarker, rawContent, updateMode, formatter, autoInsertIfMissing) {
1771
- let content = existingContent;
1772
- const diffs = [];
1773
- let hasError = false;
1774
- let wasUpdated = false;
1775
- const generatedSection = [
1776
- startMarker,
1777
- await applyFormatter(rawContent, formatter),
1778
- endMarker
1779
- ].join("\n");
1780
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
1781
- if (!existingSection) {
1782
- if (updateMode && autoInsertIfMissing) {
1783
- content = content.trimEnd() + "\n\n" + generatedSection + "\n";
1784
- wasUpdated = true;
1785
- return {
1786
- content,
1787
- diffs,
1788
- hasError,
1789
- wasUpdated
1790
- };
1791
- }
1792
- hasError = true;
1793
- diffs.push(`${markerLabel} marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
1794
- return {
1795
- content,
1796
- diffs,
1797
- hasError,
1798
- wasUpdated
1799
- };
1800
- }
1801
- if (existingSection !== generatedSection) if (updateMode) {
1802
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
1803
- if (updated) {
1804
- content = updated;
1805
- wasUpdated = true;
1806
- } else {
1807
- hasError = true;
1808
- diffs.push(`Failed to replace ${markerLabel} section`);
1809
- }
1810
- } else {
1811
- hasError = true;
1812
- diffs.push(formatDiff(existingSection, generatedSection));
1813
- }
1814
- return {
1815
- content,
1816
- diffs,
1817
- hasError,
1818
- wasUpdated
1819
- };
1820
- }
1821
- /**
1822
- * Process index marker in file content
1823
- * Returns result with updated content and any diffs.
1824
- * If the marker is not present in the file, the section is silently skipped.
1825
- */
1826
- async function processIndexMarker(existingContent, categories, command, scope, updateMode, formatter, indexOptions) {
1827
- let content = existingContent;
1828
- const diffs = [];
1829
- let hasError = false;
1830
- let wasUpdated = false;
1831
- const startMarker = indexStartMarker(scope);
1832
- const endMarker = indexEndMarker(scope);
1833
- const hasStartMarker = content.includes(startMarker);
1834
- const hasEndMarker = content.includes(endMarker);
1835
- if (!hasStartMarker && !hasEndMarker) return {
1836
- content,
1837
- diffs,
1838
- hasError,
1839
- wasUpdated
1840
- };
1841
- if (!hasStartMarker || !hasEndMarker) {
1842
- hasError = true;
1843
- diffs.push("Index marker section is malformed: both start and end markers are required.");
1844
- return {
1845
- content,
1846
- diffs,
1847
- hasError,
1848
- wasUpdated
1849
- };
1850
- }
1851
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
1852
- if (!existingSection) {
1853
- hasError = true;
1854
- diffs.push("Index marker section is malformed: start marker must appear before end marker.");
1855
- return {
1856
- content,
1857
- diffs,
1858
- hasError,
1859
- wasUpdated
1860
- };
1861
- }
1862
- const generatedSection = await applyFormatter(await generateIndexSection(categories, command, scope, indexOptions), formatter);
1863
- if (existingSection !== generatedSection) if (updateMode) {
1864
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
1865
- if (updated) {
1866
- content = updated;
1867
- wasUpdated = true;
1868
- } else {
1869
- hasError = true;
1870
- diffs.push("Failed to replace index section");
1871
- }
1872
- } else {
1873
- hasError = true;
1874
- diffs.push(formatDiff(existingSection, generatedSection));
1875
- }
1876
- return {
1877
- content,
1878
- diffs,
1879
- hasError,
1880
- wasUpdated
1881
- };
1882
- }
1883
- /**
1884
- * Find which file contains a specific command
1885
- */
1886
- function findFileForCommand(commandPath, files, allCommands, ignores) {
1887
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1888
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1889
- if (commandPaths.includes(commandPath)) return filePath;
1890
- }
1891
- return null;
1892
- }
1893
- /**
1894
- * Find which target commands are contained in a file
1895
- * Also expands each target command to include subcommands that are NOT explicitly in specifiedCommands
1896
- */
1897
- function findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) {
1898
- const fileConfigRaw = files[filePath];
1899
- if (!fileConfigRaw) return [];
1900
- const { specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1901
- const expandedTargets = /* @__PURE__ */ new Set();
1902
- for (const targetCmd of targetCommands) {
1903
- if (!commandPaths.includes(targetCmd)) continue;
1904
- expandedTargets.add(targetCmd);
1905
- for (const cmdPath of commandPaths) if (isSubcommandOf(cmdPath, targetCmd) && !specifiedCommands.includes(cmdPath)) expandedTargets.add(cmdPath);
1906
- }
1907
- return Array.from(expandedTargets);
1908
- }
1909
- /**
1910
- * Generate a single command section (already contains section markers from renderer)
1911
- */
1912
- function generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions) {
1913
- const info = allCommands.get(cmdPath);
1914
- if (!info) return null;
1915
- const enriched = {
1916
- ...info,
1917
- filePath,
1918
- fileMap,
1919
- rootDocPath
1920
- };
1921
- if (hasGlobalOptions !== void 0) enriched.hasGlobalOptions = hasGlobalOptions;
1922
- return render(enriched);
1923
- }
1924
- /**
1925
- * Generate markdown for a file containing multiple commands
1926
- * Each command section is wrapped with markers for partial validation
1927
- */
1928
- function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedOrder, fileConfig, rootDocPath, hasGlobalOptions) {
1929
- const sections = [];
1930
- const header = fileConfig ? generateFileHeader(fileConfig) : null;
1931
- if (header) sections.push(header);
1932
- const sortedPaths = sortDepthFirst(commandPaths, specifiedOrder ?? []);
1933
- for (const cmdPath of sortedPaths) {
1934
- const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions);
1935
- if (section) sections.push(section);
1936
- }
1937
- return `${sections.join("\n")}\n`;
1938
- }
1939
- /**
1940
- * Build a map of command path to file path
1941
- */
1942
- function buildFileMap(files, allCommands, ignores) {
1943
- const fileMap = {};
1944
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
1945
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
1946
- for (const cmdPath of commandPaths) fileMap[cmdPath] = filePath;
1947
- }
1948
- return fileMap;
1949
- }
1950
- /**
1951
- * Execute examples for commands based on configuration
1952
- */
1953
- async function executeConfiguredExamples(allCommands, examplesConfig, rootCommand) {
1954
- for (const [cmdPath, cmdConfig] of Object.entries(examplesConfig)) {
1955
- const commandInfo = allCommands.get(cmdPath);
1956
- if (!commandInfo?.examples?.length) continue;
1957
- const config = cmdConfig === true ? {} : cmdConfig;
1958
- const commandPath = cmdPath ? cmdPath.split(" ") : [];
1959
- commandInfo.exampleResults = await executeExamples(commandInfo.examples, config, rootCommand, commandPath);
1960
- }
1961
- }
1962
- /**
1963
- * Convert PathConfig to FileMapping with explicit command paths.
1964
- * Uses noExpand to prevent subcommand expansion since paths are pre-resolved.
1965
- */
1966
- function pathToFiles(pathConfig, allCommands) {
1967
- if (typeof pathConfig === "string") return {
1968
- files: { [pathConfig]: Array.from(allCommands.keys()) },
1969
- rootDocPath: pathConfig
1970
- };
1971
- const { root, commands = {} } = pathConfig;
1972
- const files = {};
1973
- const assignedToOtherFiles = /* @__PURE__ */ new Set();
1974
- const sortedEntries = Object.entries(commands).sort(([a], [b]) => b.split(" ").length - a.split(" ").length);
1975
- for (const [cmdPath, filePath] of sortedEntries) {
1976
- if (!files[filePath]) files[filePath] = {
1977
- commands: [],
1978
- noExpand: true
1979
- };
1980
- const fc = files[filePath];
1981
- for (const existingPath of allCommands.keys()) if ((existingPath === cmdPath || existingPath.startsWith(cmdPath + " ")) && !assignedToOtherFiles.has(existingPath)) {
1982
- fc.commands.push(existingPath);
1983
- assignedToOtherFiles.add(existingPath);
1984
- }
1985
- }
1986
- files[root] = {
1987
- commands: Array.from(allCommands.keys()).filter((p) => !assignedToOtherFiles.has(p)),
1988
- noExpand: true
1989
- };
1990
- return {
1991
- files,
1992
- rootDocPath: root
1993
- };
1994
- }
1995
- /**
1996
- * Generate documentation from command definition
1997
- */
1998
- async function generateDoc(config) {
1999
- const { command, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands, globalArgs } = config;
2000
- const allCommands = await collectAllCommands(command);
2001
- let files;
2002
- let usingPathConfig = false;
2003
- let resolvedRootDocPath;
2004
- if (config.path !== void 0) {
2005
- if (config.files !== void 0) throw new Error("Cannot specify both \"path\" and \"files\". Use one or the other.");
2006
- const converted = pathToFiles(config.path, allCommands);
2007
- files = converted.files;
2008
- resolvedRootDocPath = converted.rootDocPath;
2009
- usingPathConfig = true;
2010
- } else if (config.files !== void 0) files = config.files;
2011
- else throw new Error("Either \"path\" or \"files\" must be specified.");
2012
- let rootDoc = config.rootDoc;
2013
- if (!rootDoc && usingPathConfig && (globalArgs || config.rootInfo)) rootDoc = { path: resolvedRootDocPath };
2014
- if (globalArgs && rootDoc && !rootDoc.globalOptions) {
2015
- const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
2016
- if (optionFields.length > 0) {
2017
- const globalShape = Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
2018
- rootDoc = {
2019
- ...rootDoc,
2020
- globalOptions: globalShape
2021
- };
2022
- }
2023
- }
2024
- const updateMode = isTruthyEnv(UPDATE_GOLDEN_ENV);
2025
- const doctorMode = isTruthyEnv(DOCTOR_ENV);
2026
- let hasDoctorIssues = false;
2027
- if (rootDoc && !usingPathConfig) {
2028
- const normalizedRootDocPath = normalizeDocPathForComparison(rootDoc.path);
2029
- if (Object.keys(files).some((filePath) => normalizeDocPathForComparison(filePath) === normalizedRootDocPath)) throw new Error(`rootDoc.path "${rootDoc.path}" must not also appear as a key in files.`);
2030
- }
2031
- if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
2032
- const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
2033
- if (hasTargetCommands) {
2034
- for (const targetCommand of targetCommands) if (!findFileForCommand(targetCommand, files, allCommands, ignores)) throw new Error(`Target command "${targetCommand}" not found in any file configuration`);
2035
- }
2036
- const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
2037
- validateGlobalOptionCompatibility(hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores), allCommands, globalOptionDefinitions);
2038
- if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
2039
- const allFilesCommands = [];
2040
- for (const fileConfigRaw of Object.values(files)) {
2041
- const fileConfig = normalizeFileConfig(fileConfigRaw);
2042
- allFilesCommands.push(...fileConfig.commands);
2043
- }
2044
- validateIgnoresExist(ignores, allCommands);
2045
- validateNoConflicts(allFilesCommands, ignores, allCommands);
2046
- const fileMap = buildFileMap(files, allCommands, ignores);
2047
- const results = [];
2048
- let hasError = false;
2049
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
2050
- const { fileConfig, specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2051
- if (specifiedCommands.length === 0) continue;
2052
- if (commandPaths.length === 0) continue;
2053
- const fileTargetCommands = hasTargetCommands ? findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) : [];
2054
- if (hasTargetCommands && fileTargetCommands.length === 0) continue;
2055
- let fileStatus = "match";
2056
- const diffs = [];
2057
- const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
2058
- const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
2059
- const fileRenderer = createCommandRenderer({
2060
- ...format,
2061
- headingLevel: adjustedHeadingLevel
2062
- });
2063
- const render = fileConfig.render ?? fileRenderer;
2064
- const isRootDocFile = usingPathConfig && rootDoc && normalizeDocPathForComparison(filePath) === normalizeDocPathForComparison(rootDoc.path);
2065
- if (hasTargetCommands || isRootDocFile) {
2066
- let existingContent = readFile(filePath);
2067
- const sortedCommandPaths = sortDepthFirst(commandPaths, specifiedCommands);
2068
- const effectiveTargetCommands = hasTargetCommands ? fileTargetCommands : commandPaths;
2069
- for (const targetCommand of effectiveTargetCommands) {
2070
- const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, fileMap, rootDoc?.path, globalOptionDefinitions.size > 0);
2071
- if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
2072
- const generatedSection = await applyFormatter(rawSection, formatter);
2073
- if (!existingContent) {
2074
- if (updateMode) {
2075
- const header = targetCommand === "" && fileConfig ? generateFileHeader(fileConfig) : null;
2076
- const fullContent = header ? `${header}\n${generatedSection}` : generatedSection;
2077
- writeFile(filePath, fullContent);
2078
- existingContent = fullContent;
2079
- fileStatus = "created";
2080
- } else {
2081
- hasError = true;
2082
- fileStatus = "diff";
2083
- diffs.push(`File does not exist. Target command "${targetCommand}" section cannot be validated.`);
2084
- }
2085
- continue;
2086
- }
2087
- const existingMarkers = collectSectionMarkers(existingContent, targetCommand);
2088
- if (existingMarkers.length === 0) {
2089
- if (updateMode) {
2090
- existingContent = insertCommandSections(existingContent, targetCommand, generatedSection, sortedCommandPaths);
2091
- writeFile(filePath, existingContent);
2092
- if (fileStatus !== "created") fileStatus = "updated";
2093
- } else {
2094
- hasError = true;
2095
- fileStatus = "diff";
2096
- diffs.push(`Existing file does not contain section markers for command "${targetCommand}"`);
2097
- }
2098
- continue;
2099
- }
2100
- for (const sectionType of existingMarkers) {
2101
- const existingSection = extractSectionMarker(existingContent, sectionType, targetCommand);
2102
- const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
2103
- if (!existingSection) continue;
2104
- if (!generatedSectionPart) {
2105
- const emptyMarker = sectionStartMarker(sectionType, targetCommand) + "\n" + sectionEndMarker(sectionType, targetCommand);
2106
- if (existingSection !== emptyMarker) if (updateMode) {
2107
- const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, emptyMarker);
2108
- if (!updated) throw new Error(`Failed to replace stale ${sectionType} section for command "${targetCommand}"`);
2109
- existingContent = updated.replace(/\n{3,}/g, "\n\n");
2110
- writeFile(filePath, existingContent);
2111
- if (fileStatus !== "created") fileStatus = "updated";
2112
- } else {
2113
- hasError = true;
2114
- fileStatus = "diff";
2115
- diffs.push(formatDiff(existingSection, emptyMarker));
2116
- }
2117
- continue;
2118
- }
2119
- if (existingSection !== generatedSectionPart) if (updateMode) {
2120
- const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, generatedSectionPart);
2121
- if (updated) {
2122
- existingContent = updated;
2123
- writeFile(filePath, existingContent);
2124
- if (fileStatus !== "created") fileStatus = "updated";
2125
- } else throw new Error(`Failed to replace ${sectionType} section for command "${targetCommand}"`);
2126
- } else {
2127
- hasError = true;
2128
- fileStatus = "diff";
2129
- diffs.push(formatDiff(existingSection, generatedSectionPart));
2130
- }
2131
- }
2132
- if (doctorMode) {
2133
- const generatedMarkers = collectSectionMarkers(generatedSection, targetCommand);
2134
- const existingMarkerSet = new Set(existingMarkers);
2135
- for (const sectionType of generatedMarkers) {
2136
- if (existingMarkerSet.has(sectionType)) continue;
2137
- const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
2138
- if (!generatedSectionPart) continue;
2139
- if (updateMode) {
2140
- existingContent = insertSectionMarkerAtOrder(existingContent, sectionType, targetCommand, generatedSectionPart);
2141
- writeFile(filePath, existingContent);
2142
- if (fileStatus !== "created") fileStatus = "updated";
2143
- } else {
2144
- hasError = true;
2145
- hasDoctorIssues = true;
2146
- fileStatus = "diff";
2147
- diffs.push(`[doctor] Missing section marker "${sectionType}" for command "${formatCommandPath(targetCommand)}". Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert.\n${generatedSectionPart}`);
2148
- }
2149
- }
2150
- }
2151
- }
2152
- if (existingContent) {
2153
- const existingMarkerPaths = collectSectionMarkerPaths(existingContent);
2154
- const commandPathSet = new Set(commandPaths);
2155
- if (updateMode) {
2156
- let removedAny = false;
2157
- for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
2158
- existingContent = removeCommandSections(existingContent, markerPath);
2159
- removedAny = true;
2160
- }
2161
- if (removedAny) {
2162
- writeFile(filePath, existingContent);
2163
- if (fileStatus !== "created") fileStatus = "updated";
2164
- }
2165
- } else for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
2166
- hasError = true;
2167
- fileStatus = "diff";
2168
- diffs.push(`Found orphaned section markers for deleted command "${formatCommandPath(markerPath)}"`);
2169
- }
2170
- }
2171
- } else {
2172
- const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedCommands, fileConfig, rootDoc?.path, globalOptionDefinitions.size > 0), formatter);
2173
- const comparison = compareWithExisting(generatedMarkdown, filePath);
2174
- if (comparison.match) {} else if (updateMode) {
2175
- writeFile(filePath, generatedMarkdown);
2176
- fileStatus = comparison.fileExists ? "updated" : "created";
2177
- } else {
2178
- hasError = true;
2179
- fileStatus = "diff";
2180
- if (comparison.diff) diffs.push(comparison.diff);
2181
- }
2182
- }
2183
- if (diffs.length > 0) fileStatus = "diff";
2184
- results.push({
2185
- path: filePath,
2186
- status: fileStatus,
2187
- diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
2188
- });
2189
- }
2190
- if (rootDoc) {
2191
- const rootDocFilePath = rootDoc.path;
2192
- let rootDocStatus = "match";
2193
- const rootDocDiffs = [];
2194
- const existingContent = readFile(rootDocFilePath);
2195
- if (existingContent === null) {
2196
- hasError = true;
2197
- rootDocStatus = "diff";
2198
- rootDocDiffs.push("File does not exist. Cannot validate rootDoc markers.");
2199
- } else {
2200
- let content = existingContent;
2201
- let markerUpdated = false;
2202
- const rootInfo = config.rootInfo;
2203
- const rootDocFileConfig = { title: rootInfo?.title ?? command.name };
2204
- if (rootDoc.headingLevel !== void 0) rootDocFileConfig.headingLevel = rootDoc.headingLevel;
2205
- const rootDescription = rootInfo?.description ?? command.description;
2206
- if (rootDescription !== void 0) rootDocFileConfig.description = rootDescription;
2207
- const headerResult = processFileHeader(content, rootDocFileConfig, updateMode);
2208
- content = headerResult.content;
2209
- if (headerResult.diff) rootDocDiffs.push(headerResult.diff);
2210
- if (headerResult.hasError) hasError = true;
2211
- if (headerResult.wasUpdated) markerUpdated = true;
2212
- if (rootInfo?.header) {
2213
- const headerMarkerResult = await processStaticMarker(content, "Root header", rootHeaderStartMarker(), rootHeaderEndMarker(), rootInfo.header, updateMode, formatter, usingPathConfig);
2214
- content = headerMarkerResult.content;
2215
- rootDocDiffs.push(...headerMarkerResult.diffs);
2216
- if (headerMarkerResult.hasError) hasError = true;
2217
- if (headerMarkerResult.wasUpdated) markerUpdated = true;
2218
- }
2219
- if (!usingPathConfig) {
2220
- const unexpectedSectionPaths = collectSectionMarkerPaths(content);
2221
- if (unexpectedSectionPaths.length > 0) if (updateMode) {
2222
- for (const commandPath of unexpectedSectionPaths) content = removeCommandSections(content, commandPath);
2223
- markerUpdated = true;
2224
- } else {
2225
- hasError = true;
2226
- rootDocDiffs.push(`Found unexpected section markers in rootDoc: ${unexpectedSectionPaths.map((commandPath) => `"${formatCommandPath(commandPath)}"`).join(", ")}.`);
2227
- }
2228
- }
2229
- const normalizedGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
2230
- if (normalizedGlobalOptions) {
2231
- const globalOptionsResult = await processGlobalOptionsMarker(content, normalizedGlobalOptions, updateMode, formatter, usingPathConfig);
2232
- content = globalOptionsResult.content;
2233
- rootDocDiffs.push(...globalOptionsResult.diffs);
2234
- if (globalOptionsResult.hasError) hasError = true;
2235
- if (globalOptionsResult.wasUpdated) markerUpdated = true;
2236
- }
2237
- const derivedCategories = deriveIndexFromFiles(files, rootDocFilePath, allCommands, ignores);
2238
- const indexScope = node_path.relative(process.cwd(), rootDocFilePath).replace(/\\/g, "/");
2239
- const indexResult = await processIndexMarker(content, derivedCategories, command, indexScope, updateMode, formatter, rootDoc.index);
2240
- content = indexResult.content;
2241
- rootDocDiffs.push(...indexResult.diffs);
2242
- if (indexResult.hasError) hasError = true;
2243
- if (indexResult.wasUpdated) markerUpdated = true;
2244
- if (rootInfo?.footer) {
2245
- const footerMarkerResult = await processStaticMarker(content, "Root footer", rootFooterStartMarker(), rootFooterEndMarker(), rootInfo.footer, updateMode, formatter, usingPathConfig);
2246
- content = footerMarkerResult.content;
2247
- rootDocDiffs.push(...footerMarkerResult.diffs);
2248
- if (footerMarkerResult.hasError) hasError = true;
2249
- if (footerMarkerResult.wasUpdated) markerUpdated = true;
2250
- }
2251
- if (updateMode && markerUpdated) {
2252
- writeFile(rootDocFilePath, content);
2253
- if (rootDocStatus === "match") rootDocStatus = "updated";
2254
- }
2255
- }
2256
- if (rootDocDiffs.length > 0) rootDocStatus = "diff";
2257
- results.push({
2258
- path: rootDocFilePath,
2259
- status: rootDocStatus,
2260
- diff: rootDocDiffs.length > 0 ? rootDocDiffs.join("\n\n") : void 0
2261
- });
2262
- }
2263
- const errorHint = hasDoctorIssues ? `Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to fix missing markers.` : `Run with ${UPDATE_GOLDEN_ENV}=true to update.`;
2264
- return {
2265
- success: !hasError,
2266
- files: results,
2267
- error: hasError ? `Documentation is out of date. ${errorHint}` : void 0
2268
- };
2269
- }
2270
- /**
2271
- * Assert that documentation matches golden files
2272
- * Throws an error if there are differences and update mode is not enabled
2273
- */
2274
- async function assertDocMatch(config) {
2275
- const result = await generateDoc(config);
2276
- if (!result.success) {
2277
- const diffMessages = result.files.filter((f) => f.status === "diff").map((f) => {
2278
- let msg = `File: ${f.path}\n`;
2279
- if (f.diff) msg += f.diff;
2280
- return msg;
2281
- }).join("\n\n");
2282
- throw new Error(`Documentation does not match golden files.\n\n${diffMessages}\n\n` + (result.error ?? `Run with ${"POLITTY_DOCS_UPDATE"}=true to update the documentation.`));
2283
- }
2284
- }
2285
- /**
2286
- * Initialize documentation files by deleting them
2287
- * Only deletes when update mode is enabled (POLITTY_DOCS_UPDATE=true)
2288
- * Use this in beforeAll to ensure skipped tests don't leave stale sections
2289
- * @param config - Config containing files to initialize, or a single file path
2290
- * @param fileSystem - Optional fs implementation (useful when fs is mocked)
2291
- */
2292
- function initDocFile(config, fileSystem) {
2293
- if (!isTruthyEnv("POLITTY_DOCS_UPDATE")) return;
2294
- if (typeof config === "string") deleteFile(config, fileSystem);
2295
- else if (config.files) for (const filePath of Object.keys(config.files)) deleteFile(filePath, fileSystem);
2296
- }
2297
-
2298
- //#endregion
2299
- exports.DOCTOR_ENV = DOCTOR_ENV;
2300
- exports.GLOBAL_OPTIONS_MARKER_PREFIX = GLOBAL_OPTIONS_MARKER_PREFIX;
2301
- exports.INDEX_MARKER_PREFIX = INDEX_MARKER_PREFIX;
2302
- exports.ROOT_FOOTER_MARKER_PREFIX = ROOT_FOOTER_MARKER_PREFIX;
2303
- exports.ROOT_HEADER_MARKER_PREFIX = ROOT_HEADER_MARKER_PREFIX;
2304
- exports.SECTION_MARKER_PREFIX = SECTION_MARKER_PREFIX;
2305
- exports.SECTION_TYPES = SECTION_TYPES;
2306
- exports.UPDATE_GOLDEN_ENV = UPDATE_GOLDEN_ENV;
2307
- exports.assertDocMatch = assertDocMatch;
2308
- exports.buildCommandInfo = buildCommandInfo;
2309
- exports.collectAllCommands = collectAllCommands;
2310
- exports.compareWithExisting = compareWithExisting;
2311
- exports.createCommandRenderer = createCommandRenderer;
2312
- exports.defaultRenderers = defaultRenderers;
2313
- exports.executeExamples = executeExamples;
2314
- exports.formatDiff = formatDiff;
2315
- exports.generateDoc = generateDoc;
2316
- exports.globalOptionsEndMarker = globalOptionsEndMarker;
2317
- exports.globalOptionsStartMarker = globalOptionsStartMarker;
2318
- exports.indexEndMarker = indexEndMarker;
2319
- exports.indexStartMarker = indexStartMarker;
2320
- exports.initDocFile = initDocFile;
2321
- exports.renderArgsTable = renderArgsTable;
2322
- exports.renderArgumentsList = renderArgumentsList;
2323
- exports.renderArgumentsListFromArray = renderArgumentsListFromArray;
2324
- exports.renderArgumentsTable = renderArgumentsTable;
2325
- exports.renderArgumentsTableFromArray = renderArgumentsTableFromArray;
2326
- exports.renderCommandIndex = renderCommandIndex;
2327
- exports.renderExamplesDefault = renderExamplesDefault;
2328
- exports.renderOptionsList = renderOptionsList;
2329
- exports.renderOptionsListFromArray = renderOptionsListFromArray;
2330
- exports.renderOptionsTable = renderOptionsTable;
2331
- exports.renderOptionsTableFromArray = renderOptionsTableFromArray;
2332
- exports.renderSubcommandsTable = renderSubcommandsTable;
2333
- exports.renderSubcommandsTableFromArray = renderSubcommandsTableFromArray;
2334
- exports.renderUsage = renderUsage;
2335
- exports.resolveLazyCommand = require_schema_extractor.resolveLazyCommand;
2336
- exports.rootFooterEndMarker = rootFooterEndMarker;
2337
- exports.rootFooterStartMarker = rootFooterStartMarker;
2338
- exports.rootHeaderEndMarker = rootHeaderEndMarker;
2339
- exports.rootHeaderStartMarker = rootHeaderStartMarker;
2340
- exports.sectionEndMarker = sectionEndMarker;
2341
- exports.sectionStartMarker = sectionStartMarker;
2342
- exports.writeFile = writeFile;
2343
- //# sourceMappingURL=index.cjs.map