politty 0.8.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 (62) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/docs/index.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/{runner-D43SkHt5.js → runner-APRZYXUS.js} +74 -3
  5. package/package.json +20 -65
  6. package/dist/arg-registry-DDJpsUea.d.cts +0 -942
  7. package/dist/arg-registry-DDJpsUea.d.cts.map +0 -1
  8. package/dist/arg-registry-DDJpsUea.d.ts.map +0 -1
  9. package/dist/augment.cjs +0 -0
  10. package/dist/augment.d.cts +0 -17
  11. package/dist/augment.d.cts.map +0 -1
  12. package/dist/augment.d.ts.map +0 -1
  13. package/dist/cli.cjs +0 -54
  14. package/dist/cli.cjs.map +0 -1
  15. package/dist/cli.d.cts +0 -1
  16. package/dist/cli.js.map +0 -1
  17. package/dist/completion/index.cjs +0 -23
  18. package/dist/completion/index.d.cts +0 -3
  19. package/dist/completion-CLHO3Xaz.cjs +0 -5769
  20. package/dist/completion-CLHO3Xaz.cjs.map +0 -1
  21. package/dist/completion-DHnVx9Zk.js.map +0 -1
  22. package/dist/docs/index.cjs +0 -3127
  23. package/dist/docs/index.cjs.map +0 -1
  24. package/dist/docs/index.d.cts +0 -752
  25. package/dist/docs/index.d.cts.map +0 -1
  26. package/dist/docs/index.d.ts.map +0 -1
  27. package/dist/docs/index.js.map +0 -1
  28. package/dist/index-DKGn3lIl.d.ts.map +0 -1
  29. package/dist/index-WyViqW59.d.cts +0 -663
  30. package/dist/index-WyViqW59.d.cts.map +0 -1
  31. package/dist/index.cjs +0 -45
  32. package/dist/index.d.cts +0 -685
  33. package/dist/index.d.cts.map +0 -1
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/log-collector-DK32-73m.js.map +0 -1
  36. package/dist/log-collector-DUqC427m.cjs +0 -185
  37. package/dist/log-collector-DUqC427m.cjs.map +0 -1
  38. package/dist/prompt/clack/index.cjs +0 -33
  39. package/dist/prompt/clack/index.cjs.map +0 -1
  40. package/dist/prompt/clack/index.d.cts +0 -18
  41. package/dist/prompt/clack/index.d.cts.map +0 -1
  42. package/dist/prompt/clack/index.d.ts.map +0 -1
  43. package/dist/prompt/clack/index.js.map +0 -1
  44. package/dist/prompt/index.cjs +0 -7
  45. package/dist/prompt/index.d.cts +0 -108
  46. package/dist/prompt/index.d.cts.map +0 -1
  47. package/dist/prompt/index.d.ts.map +0 -1
  48. package/dist/prompt/inquirer/index.cjs +0 -48
  49. package/dist/prompt/inquirer/index.cjs.map +0 -1
  50. package/dist/prompt/inquirer/index.d.cts +0 -18
  51. package/dist/prompt/inquirer/index.d.cts.map +0 -1
  52. package/dist/prompt/inquirer/index.d.ts.map +0 -1
  53. package/dist/prompt/inquirer/index.js.map +0 -1
  54. package/dist/prompt-Bs9e-Em3.cjs +0 -196
  55. package/dist/prompt-Bs9e-Em3.cjs.map +0 -1
  56. package/dist/prompt-Cc8Tfmdv.js.map +0 -1
  57. package/dist/runner-D43SkHt5.js.map +0 -1
  58. package/dist/runner-DvFvokV6.cjs +0 -2865
  59. package/dist/runner-DvFvokV6.cjs.map +0 -1
  60. package/dist/schema-extractor-BxSRwLrx.cjs +0 -710
  61. package/dist/schema-extractor-BxSRwLrx.cjs.map +0 -1
  62. package/dist/schema-extractor-Dqe7_kyQ.js.map +0 -1
@@ -1,3127 +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 hasSubFile = fileMap !== void 0 && Object.prototype.hasOwnProperty.call(fileMap, subCommandPath);
491
- const subFile = hasSubFile ? fileMap[subCommandPath] : void 0;
492
- if (currentFile && subFile && currentFile !== subFile) cmdCell = `[\`${fullName}\`](${getRelativePath(currentFile, subFile)}#${anchor})`;
493
- else if (fileMap && !hasSubFile) cmdCell = `\`${fullName}\``;
494
- else cmdCell = `[\`${fullName}\`](#${anchor})`;
495
- } else cmdCell = `\`${fullName}\``;
496
- if (hasAliases) lines.push(`| ${cmdCell} | ${aliasCell} | ${desc} |`);
497
- else lines.push(`| ${cmdCell} | ${desc} |`);
498
- }
499
- return lines.join("\n");
500
- }
501
- /**
502
- * Render examples as markdown
503
- *
504
- * @example
505
- * **Basic usage**
506
- *
507
- * ```bash
508
- * $ greet World
509
- * ```
510
- *
511
- * Output:
512
- * ```
513
- * Hello, World!
514
- * ```
515
- */
516
- function renderExamplesDefault(examples, results, opts) {
517
- if (examples.length === 0) return "";
518
- const showOutput = opts?.showOutput ?? true;
519
- const prefix = opts?.commandPrefix ? `${opts.commandPrefix} ` : "";
520
- const lines = [];
521
- for (let i = 0; i < examples.length; i++) {
522
- const example = examples[i];
523
- if (!example) continue;
524
- const result = results?.[i];
525
- lines.push(`**${example.desc}**`);
526
- lines.push("");
527
- lines.push("```bash");
528
- lines.push(`$ ${prefix}${example.cmd}`);
529
- if (showOutput) {
530
- if (result) {
531
- if (result.stdout) lines.push(result.stdout);
532
- if (result.stderr) lines.push(`[stderr] ${result.stderr}`);
533
- } else if (example.output) lines.push(example.output);
534
- }
535
- lines.push("```");
536
- lines.push("");
537
- }
538
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
539
- return lines.join("\n");
540
- }
541
- /**
542
- * Wrap content with section markers
543
- */
544
- function wrapWithMarker(type, scope, content) {
545
- return `${sectionStartMarker(type, scope)}\n${content}\n${sectionEndMarker(type, scope)}`;
546
- }
547
- /**
548
- * Generate a "See Global Options" link for subcommand documentation.
549
- * Returns null for root command or when no global options exist.
550
- */
551
- function getGlobalOptionsLink(info) {
552
- if (!info.hasGlobalOptions || info.commandPath === "") return null;
553
- 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.`;
554
- }
555
- function createCommandRenderer(options = {}) {
556
- const { headingLevel = 1, optionStyle = "table", generateAnchors = true, includeSubcommandDetails = true, markerless = false, renderDescription: customRenderDescription, renderUsage: customRenderUsage, renderArguments: customRenderArguments, renderOptions: customRenderOptions, renderSubcommands: customRenderSubcommands, renderNotes: customRenderNotes, renderFooter: customRenderFooter, renderExamples: customRenderExamples } = options;
557
- const wrap = markerless ? (_type, _scope, content) => content : wrapWithMarker;
558
- return (info) => {
559
- const sections = [];
560
- const scope = info.commandPath;
561
- const effectiveLevel = Math.min(headingLevel + (info.depth - 1), 6);
562
- const h = "#".repeat(effectiveLevel);
563
- const title = info.commandPath || info.name;
564
- sections.push(wrap("heading", scope, `${h} ${title}`));
565
- {
566
- const parts = [];
567
- if (info.description) parts.push(info.description);
568
- if (info.aliases && info.aliases.length > 0) parts.push(`**Aliases:** ${info.aliases.map((a) => `\`${a}\``).join(", ")}`);
569
- if (parts.length > 0) {
570
- const context = {
571
- content: parts.join("\n\n"),
572
- heading: "",
573
- info
574
- };
575
- const content = customRenderDescription ? customRenderDescription(context) : context.content;
576
- if (content) sections.push(wrap("description", scope, content));
577
- }
578
- }
579
- {
580
- const context = {
581
- content: `**Usage**\n\n\`\`\`\n${renderUsage(info)}\n\`\`\``,
582
- heading: "**Usage**",
583
- info
584
- };
585
- const content = customRenderUsage ? customRenderUsage(context) : context.content;
586
- if (content) sections.push(wrap("usage", scope, content));
587
- }
588
- if (info.positionalArgs.length > 0) {
589
- const renderArgs = (args, opts) => {
590
- const style = opts?.style ?? optionStyle;
591
- const withHeading = opts?.withHeading ?? true;
592
- const content = style === "table" ? renderArgumentsTableFromArray(args) : renderArgumentsListFromArray(args);
593
- return withHeading ? `**Arguments**\n\n${content}` : content;
594
- };
595
- const context = {
596
- args: info.positionalArgs,
597
- render: renderArgs,
598
- heading: "**Arguments**",
599
- info
600
- };
601
- const content = customRenderArguments ? customRenderArguments(context) : renderArgs(context.args);
602
- if (content) sections.push(wrap("arguments", scope, content));
603
- }
604
- if (info.options.length > 0) {
605
- const renderOpts = (opts, renderOpts) => {
606
- const style = renderOpts?.style ?? optionStyle;
607
- const withHeading = renderOpts?.withHeading ?? true;
608
- const extracted = info.extracted;
609
- let content;
610
- if (extracted && (extracted.schemaType === "union" || extracted.schemaType === "xor") && extracted.unionOptions) content = renderUnionOptionsMarkdown(extracted, style);
611
- else if (extracted && extracted.schemaType === "discriminatedUnion" && extracted.discriminator) content = renderDiscriminatedUnionOptionsMarkdown(extracted, style);
612
- else content = style === "table" ? renderOptionsTableFromArray(opts) : renderOptionsListFromArray(opts);
613
- return withHeading ? `**Options**\n\n${content}` : content;
614
- };
615
- const context = {
616
- options: info.options,
617
- render: renderOpts,
618
- heading: "**Options**",
619
- info
620
- };
621
- const content = customRenderOptions ? customRenderOptions(context) : renderOpts(context.options);
622
- if (content) sections.push(wrap("options", scope, content));
623
- }
624
- {
625
- const globalLink = getGlobalOptionsLink(info);
626
- if (globalLink) sections.push(wrap("global-options-link", scope, globalLink));
627
- }
628
- if (info.subCommands.length > 0) {
629
- const effectiveAnchors = generateAnchors && includeSubcommandDetails;
630
- const renderSubs = (subs, opts) => {
631
- const anchors = opts?.generateAnchors ?? effectiveAnchors;
632
- const withHeading = opts?.withHeading ?? true;
633
- const content = renderSubcommandsTableFromArray(subs, info, anchors);
634
- return withHeading ? `**Commands**\n\n${content}` : content;
635
- };
636
- const context = {
637
- subcommands: info.subCommands,
638
- render: renderSubs,
639
- heading: "**Commands**",
640
- info
641
- };
642
- const content = customRenderSubcommands ? customRenderSubcommands(context) : renderSubs(context.subcommands);
643
- if (content) sections.push(wrap("subcommands", scope, content));
644
- }
645
- if (info.examples && info.examples.length > 0) {
646
- const renderEx = (examples, results, opts) => {
647
- const withHeading = opts?.withHeading ?? true;
648
- const content = renderExamplesDefault(examples, results, {
649
- commandPrefix: info.fullCommandPath,
650
- ...opts
651
- });
652
- return withHeading ? `**Examples**\n\n${content}` : content;
653
- };
654
- const context = {
655
- examples: info.examples,
656
- results: info.exampleResults,
657
- render: renderEx,
658
- heading: "**Examples**",
659
- info
660
- };
661
- const content = customRenderExamples ? customRenderExamples(context) : renderEx(context.examples, context.results);
662
- if (content) sections.push(wrap("examples", scope, content));
663
- }
664
- if (info.notes) {
665
- const context = {
666
- content: `**Notes**\n\n${info.notes}`,
667
- heading: "**Notes**",
668
- info
669
- };
670
- const content = customRenderNotes ? customRenderNotes(context) : context.content;
671
- if (content) sections.push(wrap("notes", scope, content));
672
- }
673
- {
674
- const context = {
675
- content: "",
676
- heading: "",
677
- info
678
- };
679
- const content = customRenderFooter ? customRenderFooter(context) : context.content;
680
- if (content) sections.push(content);
681
- }
682
- return sections.join("\n\n") + "\n";
683
- };
684
- }
685
- /**
686
- * Default renderers presets
687
- */
688
- const defaultRenderers = {
689
- /** Standard command documentation */
690
- command: (options) => createCommandRenderer(options),
691
- /** Table style options (default) */
692
- tableStyle: createCommandRenderer({ optionStyle: "table" }),
693
- /** List style options */
694
- listStyle: createCommandRenderer({ optionStyle: "list" })
695
- };
696
-
697
- //#endregion
698
- //#region src/docs/doc-comparator.ts
699
- /**
700
- * Compare generated content with existing file
701
- */
702
- function compareWithExisting(generatedContent, filePath) {
703
- const absolutePath = node_path.resolve(filePath);
704
- if (!node_fs.existsSync(absolutePath)) return {
705
- match: false,
706
- fileExists: false
707
- };
708
- const existingContent = node_fs.readFileSync(absolutePath, "utf-8");
709
- if (generatedContent === existingContent) return {
710
- match: true,
711
- fileExists: true
712
- };
713
- return {
714
- match: false,
715
- diff: formatDiff(existingContent, generatedContent),
716
- fileExists: true
717
- };
718
- }
719
- /**
720
- * Format diff between two strings in unified diff format
721
- */
722
- function formatDiff(expected, actual) {
723
- const expectedLines = expected.split("\n");
724
- const actualLines = actual.split("\n");
725
- const result = [];
726
- result.push("--- existing");
727
- result.push("+++ generated");
728
- result.push("");
729
- const maxLines = Math.max(expectedLines.length, actualLines.length);
730
- let inChunk = false;
731
- let chunkStart = 0;
732
- const chunk = [];
733
- const flushChunk = () => {
734
- if (chunk.length > 0) {
735
- result.push(`@@ -${chunkStart + 1},${chunk.length} @@`);
736
- result.push(...chunk);
737
- chunk.length = 0;
738
- }
739
- inChunk = false;
740
- };
741
- for (let i = 0; i < maxLines; i++) {
742
- const expectedLine = expectedLines[i];
743
- const actualLine = actualLines[i];
744
- if (expectedLine === actualLine) {
745
- if (inChunk) {
746
- chunk.push(` ${expectedLine ?? ""}`);
747
- const lastChangeIndex = chunk.findIndex((line, idx) => (line.startsWith("-") || line.startsWith("+")) && chunk.slice(idx + 1).every((l) => l.startsWith(" ")));
748
- if (lastChangeIndex !== -1 && chunk.length - lastChangeIndex > 3) flushChunk();
749
- }
750
- } else {
751
- if (!inChunk) {
752
- inChunk = true;
753
- chunkStart = i;
754
- const contextStart = Math.max(0, i - 3);
755
- for (let j = contextStart; j < i; j++) chunk.push(` ${expectedLines[j] ?? ""}`);
756
- }
757
- if (expectedLine !== void 0 && (actualLine === void 0 || expectedLine !== actualLine)) chunk.push(`-${expectedLine}`);
758
- if (actualLine !== void 0 && (expectedLine === void 0 || expectedLine !== actualLine)) chunk.push(`+${actualLine}`);
759
- }
760
- }
761
- flushChunk();
762
- return result.join("\n");
763
- }
764
- /**
765
- * Write content to file, creating directories if needed
766
- */
767
- function writeFile(filePath, content) {
768
- const absolutePath = node_path.resolve(filePath);
769
- const dir = node_path.dirname(absolutePath);
770
- if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, { recursive: true });
771
- node_fs.writeFileSync(absolutePath, content, "utf-8");
772
- }
773
- /**
774
- * Read file content if it exists
775
- * Returns null if file does not exist
776
- */
777
- function readFile(filePath) {
778
- const absolutePath = node_path.resolve(filePath);
779
- if (!node_fs.existsSync(absolutePath)) return null;
780
- return node_fs.readFileSync(absolutePath, "utf-8");
781
- }
782
- /**
783
- * Delete file if it exists
784
- * @param filePath - Path to the file to delete
785
- * @param fileSystem - Optional fs implementation (useful when fs is mocked)
786
- */
787
- function deleteFile(filePath, fileSystem = node_fs) {
788
- const absolutePath = node_path.resolve(filePath);
789
- if (fileSystem.existsSync(absolutePath)) fileSystem.unlinkSync(absolutePath);
790
- }
791
-
792
- //#endregion
793
- //#region src/docs/doc-generator.ts
794
- /**
795
- * Build CommandInfo from a command
796
- */
797
- async function buildCommandInfo(command, rootName, commandPath = []) {
798
- const extracted = require_schema_extractor.getExtractedFields(command);
799
- const positionalArgs = extracted?.fields.filter((f) => f.positional) ?? [];
800
- const options = extracted?.fields.filter((f) => !f.positional) ?? [];
801
- const subCommands = [];
802
- if (command.subCommands) for (const [name, subCmd] of Object.entries(command.subCommands)) {
803
- const resolved = await require_schema_extractor.resolveLazyCommand(subCmd);
804
- const fullPath = [...commandPath, name];
805
- subCommands.push({
806
- name,
807
- description: resolved.description,
808
- aliases: resolved.aliases,
809
- fullPath
810
- });
811
- }
812
- return {
813
- name: command.name ?? "",
814
- description: command.description,
815
- aliases: command.aliases,
816
- fullCommandPath: commandPath.length > 0 ? `${rootName} ${commandPath.join(" ")}` : rootName,
817
- commandPath: commandPath.join(" "),
818
- depth: commandPath.length + 1,
819
- positionalArgs,
820
- options,
821
- subCommands,
822
- extracted,
823
- command,
824
- notes: command.notes,
825
- examples: command.examples
826
- };
827
- }
828
- /**
829
- * Collect all commands with their paths
830
- * Returns a map of command path -> CommandInfo
831
- */
832
- async function collectAllCommands(command, rootName) {
833
- const root = rootName ?? command.name ?? "command";
834
- const result = /* @__PURE__ */ new Map();
835
- async function traverse(cmd, path) {
836
- const info = await buildCommandInfo(cmd, root, path);
837
- const pathKey = path.join(" ");
838
- result.set(pathKey, info);
839
- if (cmd.subCommands) for (const [name, subCmd] of Object.entries(cmd.subCommands)) await traverse(await require_schema_extractor.resolveLazyCommand(subCmd), [...path, name]);
840
- }
841
- await traverse(command, []);
842
- return result;
843
- }
844
-
845
- //#endregion
846
- //#region src/docs/example-executor.ts
847
- /**
848
- * Execute examples for a command and capture output
849
- *
850
- * @param examples - Examples to execute
851
- * @param config - Execution configuration (mock setup/cleanup)
852
- * @param rootCommand - Root command to execute against
853
- * @param commandPath - Command path for subcommands (e.g., ["config", "get"])
854
- * @returns Array of execution results with captured stdout/stderr
855
- */
856
- async function executeExamples(examples, config, rootCommand, commandPath = []) {
857
- const results = [];
858
- if (config.mock) await config.mock();
859
- try {
860
- for (const example of examples) {
861
- const result = await executeSingleExample(example, rootCommand, commandPath);
862
- results.push(result);
863
- }
864
- } finally {
865
- if (config.cleanup) await config.cleanup();
866
- }
867
- return results;
868
- }
869
- /**
870
- * Execute a single example and capture output
871
- */
872
- async function executeSingleExample(example, rootCommand, commandPath) {
873
- const exampleArgs = parseExampleCmd(example.cmd);
874
- const argv = [...commandPath, ...exampleArgs];
875
- const collector = require_log_collector.createLogCollector({ passthrough: false });
876
- collector.start();
877
- let success = true;
878
- try {
879
- const { runCommand } = await Promise.resolve().then(() => require("../runner-DvFvokV6.cjs")).then((n) => n.runner_exports);
880
- const result = await runCommand(rootCommand, argv);
881
- success = result.success;
882
- if (!result.success && result.error) console.error(result.error.message);
883
- } catch (error) {
884
- success = false;
885
- console.error(error instanceof Error ? error.message : String(error));
886
- } finally {
887
- collector.stop();
888
- }
889
- const logs = collector.getLogs();
890
- const stdout = logs.entries.filter((e) => e.stream === "stdout").map((e) => e.message).join("\n");
891
- const stderr = logs.entries.filter((e) => e.stream === "stderr").map((e) => e.message).join("\n");
892
- return {
893
- cmd: example.cmd,
894
- desc: example.desc,
895
- expectedOutput: example.output,
896
- stdout,
897
- stderr,
898
- success
899
- };
900
- }
901
- /**
902
- * Parse example command string into argv array
903
- * Handles quoted strings (single and double quotes)
904
- *
905
- * @example
906
- * parseExampleCmd('World') // ['World']
907
- * parseExampleCmd('--name "John Doe"') // ['--name', 'John Doe']
908
- * parseExampleCmd("--greeting 'Hello World'") // ['--greeting', 'Hello World']
909
- */
910
- function parseExampleCmd(cmd) {
911
- const args = [];
912
- let current = "";
913
- let inQuote = false;
914
- let quoteChar = "";
915
- for (let i = 0; i < cmd.length; i++) {
916
- const char = cmd[i];
917
- if ((char === "\"" || char === "'") && !inQuote) {
918
- inQuote = true;
919
- quoteChar = char;
920
- } else if (char === quoteChar && inQuote) {
921
- inQuote = false;
922
- quoteChar = "";
923
- } else if (char === " " && !inQuote) {
924
- if (current) {
925
- args.push(current);
926
- current = "";
927
- }
928
- } else current += char;
929
- }
930
- if (current) args.push(current);
931
- return args;
932
- }
933
-
934
- //#endregion
935
- //#region src/docs/render-args.ts
936
- /**
937
- * Extract ResolvedFieldMeta array from ArgsShape
938
- *
939
- * This converts a raw args shape (like `commonArgs`) into the
940
- * ResolvedFieldMeta format used by politty's rendering functions.
941
- */
942
- function extractArgsFields(args) {
943
- return require_schema_extractor.extractFields(zod.z.object(args)).fields;
944
- }
945
- /**
946
- * Render args definition as a markdown options table
947
- *
948
- * This function takes raw args definitions (like `commonArgs`) and
949
- * renders them as a markdown table suitable for documentation.
950
- *
951
- * @example
952
- * import { renderArgsTable } from "politty/docs";
953
- * import { commonArgs, workspaceArgs } from "./args";
954
- *
955
- * const table = renderArgsTable({
956
- * ...commonArgs,
957
- * ...workspaceArgs,
958
- * });
959
- * // | Option | Alias | Description | Default |
960
- * // |--------|-------|-------------|---------|
961
- * // | `--env-file <ENV_FILE>` | `-e` | Path to environment file | - |
962
- * // ...
963
- *
964
- * @param args - Args shape (Record of string keys to Zod schemas with arg() metadata)
965
- * @param options - Rendering options
966
- * @returns Rendered markdown table string
967
- */
968
- function renderArgsTable(args, options) {
969
- const optionFields = extractArgsFields(args).filter((f) => !f.positional);
970
- if (optionFields.length === 0) return "";
971
- if (options?.columns) return renderFilteredTable(optionFields, options.columns);
972
- return renderOptionsTableFromArray(optionFields);
973
- }
974
- /**
975
- * Escape markdown special characters in table cells
976
- */
977
- function escapeTableCell$1(str) {
978
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
979
- }
980
- /**
981
- * Format default value for display
982
- */
983
- function formatDefaultValue(value) {
984
- if (value === void 0) return "-";
985
- return `\`${JSON.stringify(value)}\``;
986
- }
987
- /**
988
- * Render table with filtered columns
989
- */
990
- function renderFilteredTable(options, columns) {
991
- const lines = [];
992
- const headerCells = [];
993
- const separatorCells = [];
994
- for (const col of columns) switch (col) {
995
- case "option":
996
- headerCells.push("Option");
997
- separatorCells.push("------");
998
- break;
999
- case "alias":
1000
- headerCells.push("Alias");
1001
- separatorCells.push("-----");
1002
- break;
1003
- case "description":
1004
- headerCells.push("Description");
1005
- separatorCells.push("-----------");
1006
- break;
1007
- case "required":
1008
- headerCells.push("Required");
1009
- separatorCells.push("--------");
1010
- break;
1011
- case "default":
1012
- headerCells.push("Default");
1013
- separatorCells.push("-------");
1014
- break;
1015
- case "env":
1016
- headerCells.push("Env");
1017
- separatorCells.push("---");
1018
- break;
1019
- }
1020
- lines.push(`| ${headerCells.join(" | ")} |`);
1021
- lines.push(`| ${separatorCells.join(" | ")} |`);
1022
- for (const opt of options) {
1023
- const cells = [];
1024
- for (const col of columns) switch (col) {
1025
- case "option": {
1026
- const placeholder = opt.placeholder ?? opt.cliName.toUpperCase().replace(/-/g, "_");
1027
- let optionName;
1028
- if (opt.type === "boolean") {
1029
- optionName = `\`--${opt.cliName}\``;
1030
- if (opt.negationDisplay && !opt.negationDescription) optionName += ` / \`--${opt.negationDisplay}\``;
1031
- } else optionName = `\`--${opt.cliName} <${placeholder}>\``;
1032
- cells.push(optionName);
1033
- break;
1034
- }
1035
- case "alias":
1036
- cells.push(opt.alias && opt.alias.length > 0 ? opt.alias.map((a) => `\`${a.length === 1 ? `-${a}` : `--${a}`}\``).join(", ") : "-");
1037
- break;
1038
- case "description":
1039
- cells.push(escapeTableCell$1(opt.description ?? ""));
1040
- break;
1041
- case "required":
1042
- cells.push(opt.required ? "Yes" : "No");
1043
- break;
1044
- case "default":
1045
- cells.push(formatDefaultValue(opt.defaultValue));
1046
- break;
1047
- case "env": {
1048
- const envNames = opt.env ? Array.isArray(opt.env) ? opt.env.map((e) => `\`${e}\``).join(", ") : `\`${opt.env}\`` : "-";
1049
- cells.push(envNames);
1050
- break;
1051
- }
1052
- }
1053
- lines.push(`| ${cells.join(" | ")} |`);
1054
- if (opt.type === "boolean" && opt.negationDisplay && opt.negationDescription) {
1055
- const negCells = [];
1056
- for (const col of columns) switch (col) {
1057
- case "option":
1058
- negCells.push(`\`--${opt.negationDisplay}\``);
1059
- break;
1060
- case "description":
1061
- negCells.push(`${escapeTableCell$1(opt.negationDescription)} ${negationRelationMarker(opt)}`);
1062
- break;
1063
- case "required":
1064
- negCells.push(opt.required ? "Yes" : "No");
1065
- break;
1066
- case "alias":
1067
- case "default":
1068
- case "env":
1069
- negCells.push("-");
1070
- break;
1071
- }
1072
- lines.push(`| ${negCells.join(" | ")} |`);
1073
- }
1074
- }
1075
- return lines.join("\n");
1076
- }
1077
-
1078
- //#endregion
1079
- //#region src/docs/render-index.ts
1080
- /**
1081
- * Escape markdown special characters in table cells
1082
- */
1083
- function escapeTableCell(str) {
1084
- return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
1085
- }
1086
- /**
1087
- * Generate anchor from command path
1088
- */
1089
- function generateAnchor(commandPath) {
1090
- return commandPath.replace(/\s+/g, "-").toLowerCase();
1091
- }
1092
- /**
1093
- * Check if a command is a leaf (has no subcommands)
1094
- */
1095
- function isLeafCommand(info) {
1096
- return info.subCommands.length === 0;
1097
- }
1098
- function isSubcommandOf$1(childPath, parentPath) {
1099
- if (childPath === parentPath) return true;
1100
- if (parentPath === "") return childPath !== "";
1101
- return childPath.startsWith(parentPath + " ");
1102
- }
1103
- /**
1104
- * Expand commands to include their subcommands
1105
- * If a command has subcommands, recursively find all commands under it
1106
- *
1107
- * @param commandPaths - Command paths to expand
1108
- * @param allCommands - Map of all available commands
1109
- * @param leafOnly - If true, only include leaf commands; if false, include all commands
1110
- */
1111
- function expandCommands(commandPaths, allCommands, leafOnly) {
1112
- const result = [];
1113
- for (const cmdPath of commandPaths) {
1114
- const info = allCommands.get(cmdPath);
1115
- if (!info) continue;
1116
- if (isLeafCommand(info)) result.push(cmdPath);
1117
- else for (const [path, pathInfo] of allCommands) if (cmdPath === "" ? path.length > 0 : path.startsWith(cmdPath + " ") || path === cmdPath) {
1118
- if (isLeafCommand(pathInfo) || !leafOnly) result.push(path);
1119
- }
1120
- }
1121
- return result;
1122
- }
1123
- /**
1124
- * Render a single category section
1125
- */
1126
- function renderCategory(category, allCommands, headingLevel, leafOnly) {
1127
- const h = "#".repeat(headingLevel);
1128
- const lines = [];
1129
- lines.push(`${h} [${category.title}](${category.docPath})`);
1130
- lines.push("");
1131
- lines.push(category.description);
1132
- lines.push("");
1133
- const commandPaths = category.noExpand ? category.commands : expandCommands(category.commands, allCommands, leafOnly);
1134
- let visibleCommandPaths = commandPaths;
1135
- const fallbackCommandPaths = /* @__PURE__ */ new Set();
1136
- if (category.allowedCommands) {
1137
- const allowed = new Set(category.allowedCommands);
1138
- visibleCommandPaths = commandPaths.filter((cmdPath) => allowed.has(cmdPath));
1139
- for (const configuredPath of category.commands) if (allowed.has(configuredPath) && !visibleCommandPaths.some((cmdPath) => isSubcommandOf$1(cmdPath, configuredPath))) {
1140
- visibleCommandPaths.push(configuredPath);
1141
- fallbackCommandPaths.add(configuredPath);
1142
- }
1143
- }
1144
- lines.push("| Command | Description |");
1145
- lines.push("|---------|-------------|");
1146
- for (const cmdPath of visibleCommandPaths) {
1147
- const info = allCommands.get(cmdPath);
1148
- if (!info) continue;
1149
- if (!category.noExpand && leafOnly && !fallbackCommandPaths.has(cmdPath) && !isLeafCommand(info)) continue;
1150
- const displayName = cmdPath || info.name;
1151
- const anchor = generateAnchor(displayName);
1152
- const desc = escapeTableCell(info.description ?? "");
1153
- lines.push(`| [${displayName}](${category.docPath}#${anchor}) | ${desc} |`);
1154
- }
1155
- return lines.join("\n");
1156
- }
1157
- /**
1158
- * Render command index from categories
1159
- *
1160
- * Generates a category-based index of commands with links to documentation.
1161
- *
1162
- * @example
1163
- * const categories: CommandCategory[] = [
1164
- * {
1165
- * title: "Application Commands",
1166
- * description: "Commands for managing applications.",
1167
- * commands: ["init", "generate", "apply"],
1168
- * docPath: "./cli/application.md",
1169
- * },
1170
- * ];
1171
- *
1172
- * const index = await renderCommandIndex(mainCommand, categories);
1173
- * // ### [Application Commands](./cli/application.md)
1174
- * //
1175
- * // Commands for managing applications.
1176
- * //
1177
- * // | Command | Description |
1178
- * // |---------|-------------|
1179
- * // | [init](./cli/application.md#init) | Initialize a project |
1180
- * // ...
1181
- *
1182
- * @param command - Root command to extract command information from
1183
- * @param categories - Category definitions for grouping commands
1184
- * @param options - Rendering options
1185
- * @returns Rendered markdown string
1186
- */
1187
- async function renderCommandIndex(command, categories, options) {
1188
- const headingLevel = options?.headingLevel ?? 3;
1189
- const leafOnly = options?.leafOnly ?? true;
1190
- const allCommands = await collectAllCommands(command);
1191
- const sections = [];
1192
- for (const category of categories) {
1193
- const section = renderCategory(category, allCommands, headingLevel, leafOnly);
1194
- sections.push(section);
1195
- }
1196
- return sections.join("\n\n");
1197
- }
1198
-
1199
- //#endregion
1200
- //#region src/docs/golden-test.ts
1201
- /**
1202
- * Apply formatter to content if provided
1203
- * Supports both sync and async formatters
1204
- */
1205
- async function applyFormatter(content, formatter) {
1206
- if (!formatter) return content;
1207
- const formatted = await formatter(content);
1208
- if (!content.endsWith("\n") && formatted.endsWith("\n")) return formatted.slice(0, -1);
1209
- return formatted;
1210
- }
1211
- function isTruthyEnv(envKey) {
1212
- const value = process.env[envKey];
1213
- return value === "true" || value === "1";
1214
- }
1215
- function extractYamlFrontMatter(content) {
1216
- const lines = content.split(/\r?\n/);
1217
- if (lines[0] !== "---") return null;
1218
- const frontMatterLines = [];
1219
- for (let i = 1; i < lines.length; i++) {
1220
- const line = lines[i];
1221
- if (line === "---" || line === "...") return frontMatterLines.join("\n");
1222
- frontMatterLines.push(line ?? "");
1223
- }
1224
- return null;
1225
- }
1226
- function stripPolittyFrontMatterForOutput(content) {
1227
- const lineEnding = detectLineEnding(content);
1228
- const lines = content.split(/\r?\n/);
1229
- if (lines[0] !== "---") return content;
1230
- let endIndex = -1;
1231
- for (let i = 1; i < lines.length; i++) {
1232
- const line = lines[i];
1233
- if (line === "---" || line === "...") {
1234
- endIndex = i;
1235
- break;
1236
- }
1237
- }
1238
- if (endIndex === -1) return content;
1239
- const frontMatterLines = lines.slice(1, endIndex);
1240
- const keptFrontMatterLines = [];
1241
- for (let i = 0; i < frontMatterLines.length; i++) {
1242
- const line = frontMatterLines[i] ?? "";
1243
- if (!/^politty\s*:\s*(.*)$/.test(line)) {
1244
- keptFrontMatterLines.push(line);
1245
- continue;
1246
- }
1247
- while (i + 1 < frontMatterLines.length) {
1248
- const nextLine = frontMatterLines[i + 1] ?? "";
1249
- if (nextLine.trim() !== "" && !nextLine.startsWith(" ") && !nextLine.startsWith(" ")) break;
1250
- i++;
1251
- }
1252
- }
1253
- const bodyLines = lines.slice(endIndex + 1);
1254
- if (!keptFrontMatterLines.some((line) => line.trim() !== "")) return bodyLines.join(lineEnding).replace(new RegExp(`^${lineEnding}`), "");
1255
- return [
1256
- "---",
1257
- ...keptFrontMatterLines,
1258
- lines[endIndex] ?? "---",
1259
- ...bodyLines
1260
- ].join(lineEnding);
1261
- }
1262
- function stripYamlScalarQuotes(value) {
1263
- const trimmed = value.trim();
1264
- if (trimmed.length >= 2 && (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
1265
- return trimmed;
1266
- }
1267
- function normalizeTemplatePlaceholderKey(value) {
1268
- let normalized = stripYamlScalarQuotes(value);
1269
- if (normalized === "") return null;
1270
- const fullPlaceholder = normalized.match(/^\{\{politty:([^{}]*)\}\}$/);
1271
- if (fullPlaceholder) normalized = fullPlaceholder[1] ?? "";
1272
- else if (normalized.startsWith("politty:")) normalized = normalized.slice(8);
1273
- return normalized === "" ? null : normalized;
1274
- }
1275
- function templatePlaceholderKey(placeholder) {
1276
- return placeholder.slice(2, -2).slice(8);
1277
- }
1278
- function splitFrontMatterListValue(value) {
1279
- const trimmed = value.trim();
1280
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [trimmed];
1281
- return trimmed.slice(1, -1).split(",").map((item) => item.trim()).filter((item) => item.length > 0);
1282
- }
1283
- function addTemplatePlaceholderExclusion(exclusions, value) {
1284
- const normalized = normalizeTemplatePlaceholderKey(value);
1285
- if (normalized !== null) exclusions.add(normalized);
1286
- }
1287
- function collectExcludedTemplatePlaceholders(templateContent) {
1288
- const exclusions = /* @__PURE__ */ new Set();
1289
- const frontMatter = extractYamlFrontMatter(templateContent);
1290
- if (frontMatter === null) return exclusions;
1291
- let inPolittyBlock = false;
1292
- let inExcludeList = false;
1293
- let excludeIndent = 0;
1294
- for (const line of frontMatter.split(/\r?\n/)) {
1295
- const trimmed = line.trim();
1296
- if (trimmed === "" || trimmed.startsWith("#")) continue;
1297
- const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
1298
- if (topLevelPolitty) {
1299
- inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
1300
- inExcludeList = false;
1301
- continue;
1302
- }
1303
- if (!line.startsWith(" ") && !line.startsWith(" ")) {
1304
- inPolittyBlock = false;
1305
- inExcludeList = false;
1306
- continue;
1307
- }
1308
- if (!inPolittyBlock) continue;
1309
- const excludeEntry = line.match(/^(\s+)(?:exclude|excludes)\s*:\s*(.*)$/);
1310
- if (excludeEntry) {
1311
- const value = excludeEntry[2]?.trim() ?? "";
1312
- if (value === "") {
1313
- inExcludeList = true;
1314
- excludeIndent = excludeEntry[1]?.length ?? 0;
1315
- } else {
1316
- inExcludeList = false;
1317
- for (const item of splitFrontMatterListValue(value)) addTemplatePlaceholderExclusion(exclusions, item);
1318
- }
1319
- continue;
1320
- }
1321
- if (!inExcludeList) continue;
1322
- const listItem = line.match(/^(\s*)-\s*(.+)$/);
1323
- if (!listItem || (listItem[1]?.length ?? 0) <= excludeIndent) {
1324
- inExcludeList = false;
1325
- continue;
1326
- }
1327
- addTemplatePlaceholderExclusion(exclusions, listItem[2] ?? "");
1328
- }
1329
- return exclusions;
1330
- }
1331
- function collectTemplateIndexMetadata(templateContent) {
1332
- const frontMatter = extractYamlFrontMatter(templateContent);
1333
- if (frontMatter === null) return {};
1334
- let inPolittyBlock = false;
1335
- let inIndexBlock = false;
1336
- let indexIndent = 0;
1337
- const metadata = {};
1338
- for (const line of frontMatter.split(/\r?\n/)) {
1339
- const trimmed = line.trim();
1340
- if (trimmed === "" || trimmed.startsWith("#")) continue;
1341
- const topLevelPolitty = line.match(/^politty\s*:\s*(.*)$/);
1342
- if (topLevelPolitty) {
1343
- inPolittyBlock = (topLevelPolitty[1]?.trim() ?? "") === "";
1344
- inIndexBlock = false;
1345
- continue;
1346
- }
1347
- if (!line.startsWith(" ") && !line.startsWith(" ")) {
1348
- inPolittyBlock = false;
1349
- inIndexBlock = false;
1350
- continue;
1351
- }
1352
- if (!inPolittyBlock) continue;
1353
- const indexEntry = line.match(/^(\s+)index\s*:\s*(.*)$/);
1354
- if (indexEntry) {
1355
- inIndexBlock = (indexEntry[2]?.trim() ?? "") === "";
1356
- indexIndent = indexEntry[1]?.length ?? 0;
1357
- continue;
1358
- }
1359
- if (!inIndexBlock) continue;
1360
- if ((line.match(/^(\s*)/)?.[1]?.length ?? 0) <= indexIndent) {
1361
- inIndexBlock = false;
1362
- continue;
1363
- }
1364
- const property = line.match(/^\s+(title|description)\s*:\s*(.+)$/);
1365
- if (!property) continue;
1366
- const key = property[1];
1367
- const value = stripYamlScalarQuotes(property[2] ?? "");
1368
- if (key === "title") metadata.title = value;
1369
- else if (key === "description") metadata.description = value;
1370
- }
1371
- return metadata;
1372
- }
1373
- function createTemplateExclusions(rawKeys) {
1374
- return {
1375
- rawKeys,
1376
- commandScopes: /* @__PURE__ */ new Set(),
1377
- commandSections: /* @__PURE__ */ new Map(),
1378
- globalOptions: false,
1379
- index: false
1380
- };
1381
- }
1382
- function setFileMapEntry(fileMap, commandPath, filePath) {
1383
- Object.defineProperty(fileMap, commandPath, {
1384
- value: filePath,
1385
- enumerable: true,
1386
- configurable: true,
1387
- writable: true
1388
- });
1389
- }
1390
- /**
1391
- * Normalize file mapping entry to FileConfig
1392
- */
1393
- function normalizeFileConfig(config) {
1394
- if (Array.isArray(config)) return { commands: config };
1395
- 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.");
1396
- return config;
1397
- }
1398
- /**
1399
- * Check if a command path is a subcommand of another
1400
- */
1401
- function isSubcommandOf(childPath, parentPath) {
1402
- if (parentPath === "") return true;
1403
- if (childPath === parentPath) return true;
1404
- return childPath.startsWith(parentPath + " ");
1405
- }
1406
- /**
1407
- * Check if a pattern contains wildcards
1408
- */
1409
- function containsWildcard(pattern) {
1410
- return pattern.includes("*");
1411
- }
1412
- /**
1413
- * Check if a command path matches a wildcard pattern
1414
- * - `*` matches any single command segment
1415
- * - Pattern segments are space-separated
1416
- *
1417
- * @example
1418
- * matchesWildcard("config get", "* *") // true
1419
- * matchesWildcard("config", "* *") // false
1420
- * matchesWildcard("config get", "config *") // true
1421
- * matchesWildcard("greet", "*") // true
1422
- */
1423
- function matchesWildcard(path, pattern) {
1424
- const pathSegments = path === "" ? [] : path.split(" ");
1425
- const patternSegments = pattern === "" ? [] : pattern.split(" ");
1426
- if (pathSegments.length !== patternSegments.length) return false;
1427
- for (let i = 0; i < patternSegments.length; i++) {
1428
- const patternSeg = patternSegments[i];
1429
- const pathSeg = pathSegments[i];
1430
- if (patternSeg !== "*" && patternSeg !== pathSeg) return false;
1431
- }
1432
- return true;
1433
- }
1434
- /**
1435
- * Expand a wildcard pattern to matching command paths
1436
- */
1437
- function expandWildcardPattern(pattern, allCommands) {
1438
- const matches = [];
1439
- for (const cmdPath of allCommands.keys()) if (matchesWildcard(cmdPath, pattern)) matches.push(cmdPath);
1440
- return matches;
1441
- }
1442
- /**
1443
- * Check if a path matches any ignore pattern (with wildcard support)
1444
- * For wildcard patterns, also ignores subcommands of matched commands
1445
- */
1446
- function matchesIgnorePattern(path, ignorePattern) {
1447
- if (containsWildcard(ignorePattern)) {
1448
- if (matchesWildcard(path, ignorePattern)) return true;
1449
- const pathSegments = path === "" ? [] : path.split(" ");
1450
- const patternSegments = ignorePattern === "" ? [] : ignorePattern.split(" ");
1451
- if (pathSegments.length > patternSegments.length) return matchesWildcard(pathSegments.slice(0, patternSegments.length).join(" "), ignorePattern);
1452
- return false;
1453
- }
1454
- return isSubcommandOf(path, ignorePattern);
1455
- }
1456
- /**
1457
- * Expand command paths to include all subcommands (with wildcard support)
1458
- */
1459
- function expandCommandPaths(commandPaths, allCommands) {
1460
- const expanded = /* @__PURE__ */ new Set();
1461
- const resolved = commandPaths.flatMap((cmdPath) => containsWildcard(cmdPath) ? expandWildcardPattern(cmdPath, allCommands) : [cmdPath]);
1462
- for (const cmdPath of resolved) for (const existingPath of allCommands.keys()) if (isSubcommandOf(existingPath, cmdPath)) expanded.add(existingPath);
1463
- return Array.from(expanded);
1464
- }
1465
- /**
1466
- * Filter out ignored commands (with wildcard support)
1467
- */
1468
- function filterIgnoredCommands(commandPaths, ignores) {
1469
- return commandPaths.filter((path) => {
1470
- return !ignores.some((ignorePattern) => matchesIgnorePattern(path, ignorePattern));
1471
- });
1472
- }
1473
- /**
1474
- * Resolve wildcards to direct matches without subcommand expansion.
1475
- * Returns the "top-level" commands for use in CommandCategory.commands,
1476
- * where expandCommands in render-index handles subcommand expansion.
1477
- */
1478
- function resolveTopLevelCommands(specifiedCommands, allCommands) {
1479
- const result = [];
1480
- for (const cmdPath of specifiedCommands) if (containsWildcard(cmdPath)) result.push(...expandWildcardPattern(cmdPath, allCommands));
1481
- else if (allCommands.has(cmdPath)) result.push(cmdPath);
1482
- return result;
1483
- }
1484
- /**
1485
- * Resolve file command configuration to concrete command paths.
1486
- * This applies wildcard/subcommand expansion and ignore filtering.
1487
- */
1488
- function resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores) {
1489
- const fileConfig = normalizeFileConfig(fileConfigRaw);
1490
- const specifiedCommands = fileConfig.commands;
1491
- return {
1492
- fileConfig,
1493
- specifiedCommands,
1494
- commandPaths: filterIgnoredCommands(fileConfig.noExpand ? specifiedCommands.filter((p) => allCommands.has(p)) : expandCommandPaths(specifiedCommands, allCommands), ignores),
1495
- topLevelCommands: filterIgnoredCommands(resolveTopLevelCommands(specifiedCommands, allCommands), ignores)
1496
- };
1497
- }
1498
- /**
1499
- * Validate that there are no conflicts between files and ignores (with wildcard support)
1500
- */
1501
- function validateNoConflicts(filesCommands, ignores, allCommands) {
1502
- const conflicts = [];
1503
- for (const filePattern of filesCommands) {
1504
- const filePaths = containsWildcard(filePattern) ? expandWildcardPattern(filePattern, allCommands) : [filePattern];
1505
- for (const filePath of filePaths) for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
1506
- if (matchesWildcard(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
1507
- } else if (filePath === ignorePattern || isSubcommandOf(filePath, ignorePattern)) conflicts.push(`"${filePath}" is both in files and ignored by "${ignorePattern}"`);
1508
- }
1509
- if (conflicts.length > 0) throw new Error(`Conflict between files and ignores:\n - ${conflicts.join("\n - ")}`);
1510
- }
1511
- /**
1512
- * Validate that all ignored paths exist in the command tree (with wildcard support)
1513
- */
1514
- function validateIgnoresExist(ignores, allCommands) {
1515
- const nonExistent = [];
1516
- for (const ignorePattern of ignores) if (containsWildcard(ignorePattern)) {
1517
- if (expandWildcardPattern(ignorePattern, allCommands).length === 0) nonExistent.push(`"${ignorePattern}"`);
1518
- } else if (!allCommands.has(ignorePattern)) nonExistent.push(`"${ignorePattern}"`);
1519
- if (nonExistent.length > 0) throw new Error(`Ignored command paths do not exist: ${nonExistent.join(", ")}`);
1520
- }
1521
- /**
1522
- * Sort command paths in depth-first order while preserving the specified command order
1523
- * Parent commands are immediately followed by their subcommands
1524
- */
1525
- function sortDepthFirst(commandPaths, specifiedOrder) {
1526
- const pathSet = new Set(commandPaths);
1527
- const topLevelPaths = specifiedOrder.filter((cmd) => pathSet.has(cmd));
1528
- for (const path of commandPaths) if ((path === "" ? 0 : path.split(" ").length) === 1 && !topLevelPaths.includes(path)) topLevelPaths.push(path);
1529
- const result = [];
1530
- const visited = /* @__PURE__ */ new Set();
1531
- function addWithChildren(cmdPath) {
1532
- if (visited.has(cmdPath) || !pathSet.has(cmdPath)) return;
1533
- visited.add(cmdPath);
1534
- result.push(cmdPath);
1535
- const children = commandPaths.filter((p) => {
1536
- if (p === cmdPath || visited.has(p)) return false;
1537
- if (cmdPath === "") return p.split(" ").length === 1;
1538
- return p.startsWith(cmdPath + " ") && p.split(" ").length === cmdPath.split(" ").length + 1;
1539
- }).sort((a, b) => a.localeCompare(b));
1540
- for (const child of children) addWithChildren(child);
1541
- }
1542
- for (const topLevel of topLevelPaths) addWithChildren(topLevel);
1543
- for (const path of commandPaths) if (!visited.has(path)) result.push(path);
1544
- return result;
1545
- }
1546
- function generateFileHeader(fileConfig) {
1547
- if (!fileConfig.title && !fileConfig.description) return null;
1548
- const parts = [];
1549
- if (fileConfig.title) {
1550
- const heading = "#".repeat(fileConfig.headingLevel ?? 1);
1551
- parts.push(`${heading} ${fileConfig.title}`);
1552
- }
1553
- if (fileConfig.description) {
1554
- parts.push("");
1555
- parts.push(fileConfig.description);
1556
- }
1557
- parts.push("");
1558
- return parts.join("\n");
1559
- }
1560
- /**
1561
- * Extract a leading file header (title and optional description paragraph)
1562
- */
1563
- function extractFileHeader(content) {
1564
- if (!/^#{1,6} /.test(content)) return null;
1565
- const titleEnd = content.indexOf("\n");
1566
- if (titleEnd === -1) return content;
1567
- let cursor = titleEnd + 1;
1568
- if (content[cursor] === "\n") cursor += 1;
1569
- while (cursor < content.length) {
1570
- const lineEnd = content.indexOf("\n", cursor);
1571
- const line = lineEnd === -1 ? content.slice(cursor) : content.slice(cursor, lineEnd);
1572
- if (line.length === 0 || /^#{1,6}\s/.test(line) || line.startsWith("<!-- politty:")) break;
1573
- cursor = lineEnd === -1 ? content.length : lineEnd + 1;
1574
- }
1575
- return content.slice(0, cursor);
1576
- }
1577
- /**
1578
- * Validate and optionally update configured file header
1579
- */
1580
- function processFileHeader(existingContent, fileConfig, updateMode) {
1581
- const generatedHeader = generateFileHeader(fileConfig);
1582
- if (!generatedHeader) return {
1583
- content: existingContent,
1584
- hasError: false,
1585
- wasUpdated: false
1586
- };
1587
- if (existingContent.startsWith(generatedHeader)) return {
1588
- content: existingContent,
1589
- hasError: false,
1590
- wasUpdated: false
1591
- };
1592
- const existingHeader = extractFileHeader(existingContent) ?? "";
1593
- if (!updateMode) return {
1594
- content: existingContent,
1595
- diff: formatDiff(existingHeader, generatedHeader),
1596
- hasError: true,
1597
- wasUpdated: false
1598
- };
1599
- return {
1600
- content: `${generatedHeader}${(existingHeader ? existingContent.slice(existingHeader.length) : existingContent).replace(/^\n+/, "")}`,
1601
- hasError: false,
1602
- wasUpdated: true
1603
- };
1604
- }
1605
- function formatCommandPath(commandPath) {
1606
- return commandPath === "" ? "<root>" : commandPath;
1607
- }
1608
- /**
1609
- * Extract a section marker's content from document content.
1610
- * Returns the content between start and end markers (including markers).
1611
- */
1612
- function extractSectionMarker(content, type, scope) {
1613
- return extractMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope));
1614
- }
1615
- /**
1616
- * Replace a section marker's content in document content.
1617
- * Returns updated content, or null if marker not found.
1618
- */
1619
- function replaceSectionMarker(content, type, scope, newContent) {
1620
- return replaceMarkerSection(content, sectionStartMarker(type, scope), sectionEndMarker(type, scope), newContent);
1621
- }
1622
- /**
1623
- * Insert a new section marker into existing content at the correct position
1624
- * relative to other section markers for the same command, based on SECTION_TYPES order.
1625
- * Preserves any existing content between adjacent markers by wrapping it with the new markers
1626
- * instead of replacing it with generated content.
1627
- * @throws If no adjacent marker is found (unreachable when at least one marker exists for the command)
1628
- */
1629
- function insertSectionMarkerAtOrder(content, type, scope, generatedSection) {
1630
- const typeIndex = SECTION_TYPES.indexOf(type);
1631
- const startMarker = sectionStartMarker(type, scope);
1632
- const endMarker = sectionEndMarker(type, scope);
1633
- let prevBoundary = null;
1634
- for (let i = typeIndex - 1; i >= 0; i--) {
1635
- const prevType = SECTION_TYPES[i];
1636
- const prevEnd = sectionEndMarker(prevType, scope);
1637
- const prevEndIdx = content.indexOf(prevEnd);
1638
- if (prevEndIdx !== -1) {
1639
- prevBoundary = prevEndIdx + prevEnd.length;
1640
- break;
1641
- }
1642
- }
1643
- let nextBoundary = null;
1644
- for (let i = typeIndex + 1; i < SECTION_TYPES.length; i++) {
1645
- const nextType = SECTION_TYPES[i];
1646
- const nextStart = sectionStartMarker(nextType, scope);
1647
- const nextStartIdx = content.indexOf(nextStart);
1648
- if (nextStartIdx !== -1) {
1649
- nextBoundary = nextStartIdx;
1650
- break;
1651
- }
1652
- }
1653
- if (prevBoundary != null && nextBoundary != null) {
1654
- const wrapped = startMarker + content.slice(prevBoundary, nextBoundary).replace(/^\n+/, "\n").replace(/\n+$/, "\n") + endMarker;
1655
- return content.slice(0, prevBoundary) + "\n\n" + wrapped + "\n\n" + content.slice(nextBoundary);
1656
- }
1657
- if (prevBoundary != null) {
1658
- let afterPos = prevBoundary;
1659
- while (afterPos < content.length && content[afterPos] === "\n") afterPos++;
1660
- return content.slice(0, prevBoundary) + "\n\n" + generatedSection + (afterPos < content.length ? "\n\n" : "\n") + content.slice(afterPos);
1661
- }
1662
- if (nextBoundary != null) {
1663
- let beforePos = nextBoundary;
1664
- while (beforePos > 0 && content[beforePos - 1] === "\n") beforePos--;
1665
- const prefix = beforePos === 0 ? "" : "\n\n";
1666
- return content.slice(0, beforePos) + prefix + generatedSection + "\n\n" + content.slice(nextBoundary);
1667
- }
1668
- 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.`);
1669
- }
1670
- /**
1671
- * Collect all section types that have markers for a given command path.
1672
- */
1673
- function collectSectionMarkers(content, commandPath) {
1674
- const found = [];
1675
- for (const type of SECTION_TYPES) if (extractSectionMarker(content, type, commandPath) !== null) found.push(type);
1676
- return found;
1677
- }
1678
- /**
1679
- * Collect all command paths that have any section markers in the content.
1680
- */
1681
- function collectSectionMarkerPaths(content) {
1682
- const sectionTypes = SECTION_TYPES.join("|");
1683
- const markerPattern = new RegExp(`<!--\\s*politty:command:(.*?):(?:${sectionTypes}):start\\s*-->`, "g");
1684
- const paths = /* @__PURE__ */ new Set();
1685
- for (const match of content.matchAll(markerPattern)) paths.add(match[1] ?? "");
1686
- return Array.from(paths);
1687
- }
1688
- /**
1689
- * Insert command section markers at the correct position based on specified order.
1690
- * Uses the heading marker of adjacent commands as reference points.
1691
- */
1692
- function insertCommandSections(content, commandPath, newSection, specifiedOrder) {
1693
- const targetIndex = specifiedOrder.indexOf(commandPath);
1694
- if (targetIndex === -1) return content.trimEnd() + "\n\n" + newSection + "\n";
1695
- for (let i = targetIndex + 1; i < specifiedOrder.length; i++) {
1696
- const nextCmd = specifiedOrder[i];
1697
- if (nextCmd === void 0) continue;
1698
- const nextMarker = sectionStartMarker("heading", nextCmd);
1699
- const nextIndex = content.indexOf(nextMarker);
1700
- if (nextIndex !== -1) {
1701
- let insertPos = nextIndex;
1702
- while (insertPos > 0 && content[insertPos - 1] === "\n") insertPos--;
1703
- if (insertPos < nextIndex) insertPos++;
1704
- return content.slice(0, insertPos) + newSection + "\n" + content.slice(nextIndex);
1705
- }
1706
- }
1707
- for (let i = targetIndex - 1; i >= 0; i--) {
1708
- const prevCmd = specifiedOrder[i];
1709
- if (prevCmd === void 0) continue;
1710
- const prevMarkers = collectSectionMarkers(content, prevCmd);
1711
- if (prevMarkers.length > 0) {
1712
- const lastType = prevMarkers[prevMarkers.length - 1];
1713
- const prevEndMarker = sectionEndMarker(lastType, prevCmd);
1714
- const prevEndIndex = content.indexOf(prevEndMarker);
1715
- if (prevEndIndex !== -1) {
1716
- const insertPos = prevEndIndex + prevEndMarker.length;
1717
- return content.slice(0, insertPos) + "\n" + newSection + content.slice(insertPos);
1718
- }
1719
- }
1720
- }
1721
- return content.trimEnd() + "\n" + newSection + "\n";
1722
- }
1723
- /**
1724
- * Remove all section markers for a command from content.
1725
- * Returns the content with all markers for the command removed and excess blank lines cleaned up.
1726
- */
1727
- function removeCommandSections(content, commandPath) {
1728
- const markers = collectSectionMarkers(content, commandPath);
1729
- for (const type of markers) {
1730
- const start = sectionStartMarker(type, commandPath);
1731
- const end = sectionEndMarker(type, commandPath);
1732
- let startIndex = content.indexOf(start);
1733
- while (startIndex !== -1) {
1734
- const endIndex = content.indexOf(end, startIndex);
1735
- if (endIndex === -1) break;
1736
- content = content.slice(0, startIndex) + content.slice(endIndex + end.length);
1737
- startIndex = content.indexOf(start, startIndex);
1738
- }
1739
- }
1740
- content = content.replace(/\n{3,}/g, "\n\n");
1741
- return content;
1742
- }
1743
- /**
1744
- * Strip politty marker lines from content, then collapse the blank-line gaps the removed markers
1745
- * leave behind (outside fenced code blocks only, so intentional blank lines inside generated
1746
- * example/code blocks are preserved) and trim leading/trailing blank lines.
1747
- */
1748
- function stripPolittyMarkers(content) {
1749
- let result = collapseBlankLinesOutsideCodeFences(content.split("\n").filter((line) => !/^<!-- politty:.*-->$/.test(line.trim())).join("\n"));
1750
- result = result.replace(/^\n+/, "").replace(/\n+$/, "");
1751
- return result;
1752
- }
1753
- /**
1754
- * Collapse runs of 3+ newlines to 2, but only outside fenced code blocks so that intentional
1755
- * blank lines inside handwritten code samples are preserved. Fences are lines whose trimmed
1756
- * content starts with ``` or ~~~.
1757
- */
1758
- function collapseBlankLinesOutsideCodeFences(content) {
1759
- const lines = content.split("\n");
1760
- const out = [];
1761
- let inFence = false;
1762
- let blankRun = 0;
1763
- for (const line of lines) {
1764
- const trimmed = line.trim();
1765
- if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
1766
- inFence = !inFence;
1767
- blankRun = 0;
1768
- out.push(line);
1769
- continue;
1770
- }
1771
- if (!inFence && line.trim() === "") {
1772
- blankRun++;
1773
- if (blankRun >= 2) continue;
1774
- } else if (!inFence) blankRun = 0;
1775
- out.push(line);
1776
- }
1777
- return out.join("\n");
1778
- }
1779
- function detectLineEnding(content) {
1780
- return content.includes("\r\n") ? "\r\n" : "\n";
1781
- }
1782
- function countLineBreaks(value) {
1783
- return (value.match(/\n/g) ?? []).length;
1784
- }
1785
- /**
1786
- * Type guard for SectionType values parsed from template placeholders.
1787
- */
1788
- function isSectionType(value) {
1789
- return SECTION_TYPES.some((type) => type === value);
1790
- }
1791
- /**
1792
- * Clamp a numeric heading level to the valid HeadingLevel range (1–6).
1793
- * Uses a switch to return a literal union member, avoiding `as` assertions.
1794
- */
1795
- function clampHeadingLevel(level) {
1796
- switch (Math.min(6, Math.max(1, Math.trunc(level)))) {
1797
- case 1: return 1;
1798
- case 2: return 2;
1799
- case 3: return 3;
1800
- case 4: return 4;
1801
- case 5: return 5;
1802
- default: return 6;
1803
- }
1804
- }
1805
- function resolveTemplateCommandScope(tokens, allCommands) {
1806
- if (tokens.length === 0) return allCommands === void 0 || allCommands.has("") ? "" : null;
1807
- const exactScope = tokens.join(":");
1808
- if (allCommands?.has(exactScope)) return exactScope;
1809
- const colonSeparatedScope = tokens.join(" ");
1810
- if (allCommands?.has(colonSeparatedScope)) return colonSeparatedScope;
1811
- return allCommands === void 0 ? colonSeparatedScope : null;
1812
- }
1813
- function templateScopeFallback(tokens) {
1814
- return tokens.join(" ");
1815
- }
1816
- /**
1817
- * Parse a single {{politty:...}} placeholder string into a discriminated structure.
1818
- * The `placeholder` argument should be the full `{{politty:...}}` text.
1819
- *
1820
- * Uses String.match / String.replace internally (not .exec) to avoid lastIndex
1821
- * state issues from the shared TEMPLATE_PLACEHOLDER_REGEX constant.
1822
- */
1823
- function parsePlaceholder(placeholder, allCommands) {
1824
- const tokens = placeholder.slice(2, -2).split(":");
1825
- const directive = tokens[1];
1826
- if (directive === "command") {
1827
- const rest = tokens.slice(2);
1828
- if (rest.length === 1 && rest[0] === "") return {
1829
- kind: "invalid",
1830
- reason: `Trailing colon in "${placeholder}"; use {{politty:command}} for the root command.`
1831
- };
1832
- const fullScope = resolveTemplateCommandScope(rest, allCommands);
1833
- if (fullScope !== null) return {
1834
- kind: "command",
1835
- scope: fullScope,
1836
- type: void 0
1837
- };
1838
- if (rest.length >= 2) {
1839
- const last = rest[rest.length - 1];
1840
- const scopeTokens = rest.slice(0, -1);
1841
- const sectionScope = resolveTemplateCommandScope(scopeTokens, allCommands);
1842
- if (last !== void 0 && isSectionType(last)) return {
1843
- kind: "command",
1844
- scope: sectionScope ?? templateScopeFallback(scopeTokens),
1845
- type: last
1846
- };
1847
- if (last !== void 0 && sectionScope !== null) return {
1848
- kind: "invalid",
1849
- reason: `Unknown section type "${last}" for command scope "${formatCommandPath(sectionScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`
1850
- };
1851
- }
1852
- return {
1853
- kind: "command",
1854
- scope: templateScopeFallback(rest),
1855
- type: void 0
1856
- };
1857
- }
1858
- if (directive === "global-options") {
1859
- if (tokens.length !== 2) return {
1860
- kind: "invalid",
1861
- reason: `Malformed placeholder "${placeholder}". Expected {{politty:global-options}}.`
1862
- };
1863
- return { kind: "global-options" };
1864
- }
1865
- if (directive === "index") {
1866
- if (tokens.length !== 2) return {
1867
- kind: "invalid",
1868
- reason: `Malformed placeholder "${placeholder}". Expected {{politty:index}}.`
1869
- };
1870
- return { kind: "index" };
1871
- }
1872
- return {
1873
- kind: "invalid",
1874
- reason: `Unknown politty directive "${directive ?? ""}" in "${placeholder}". Valid directives: command, global-options, index`
1875
- };
1876
- }
1877
- function buildTemplateExclusions(rawKeys, allCommands) {
1878
- const exclusions = createTemplateExclusions(rawKeys);
1879
- for (const key of rawKeys) {
1880
- const parsed = parsePlaceholder(`{{politty:${key}}}`, allCommands);
1881
- if (parsed.kind === "command") if (parsed.type === void 0) exclusions.commandScopes.add(parsed.scope);
1882
- else {
1883
- let sections = exclusions.commandSections.get(parsed.scope);
1884
- if (!sections) {
1885
- sections = /* @__PURE__ */ new Set();
1886
- exclusions.commandSections.set(parsed.scope, sections);
1887
- }
1888
- sections.add(parsed.type);
1889
- }
1890
- else if (parsed.kind === "global-options") exclusions.globalOptions = true;
1891
- else if (parsed.kind === "index") exclusions.index = true;
1892
- }
1893
- return exclusions;
1894
- }
1895
- function isCommandScopeExcluded(commandPath, excludedCommandScopes) {
1896
- for (const excludedScope of excludedCommandScopes) if (isSubcommandOf(commandPath, excludedScope)) return true;
1897
- return false;
1898
- }
1899
- function isCommandSectionExcluded(commandPath, sectionType, exclusions) {
1900
- if (isCommandScopeExcluded(commandPath, exclusions.commandScopes)) return true;
1901
- return exclusions.commandSections.get(commandPath)?.has(sectionType) ?? false;
1902
- }
1903
- function getTemplateCommandTreePaths(commandPath, allCommands, ignores, exclusions) {
1904
- return sortDepthFirst(filterIgnoredCommands(expandCommandPaths([commandPath], allCommands), ignores).filter((path) => !isCommandScopeExcluded(path, exclusions.commandScopes)), [commandPath]);
1905
- }
1906
- function shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions) {
1907
- if (exclusions.rawKeys.has(templatePlaceholderKey(placeholder))) return true;
1908
- if (parsed.kind === "command") {
1909
- if (isCommandScopeExcluded(parsed.scope, exclusions.commandScopes)) return true;
1910
- return parsed.type !== void 0 && (exclusions.commandSections.get(parsed.scope)?.has(parsed.type) ?? false);
1911
- }
1912
- if (parsed.kind === "global-options") return exclusions.globalOptions;
1913
- if (parsed.kind === "index") return exclusions.index;
1914
- return false;
1915
- }
1916
- function isRawCommandPlaceholderUnderExcludedScope(key, exclusions) {
1917
- if (!key.startsWith("command:")) return false;
1918
- const tokens = key.slice(8).split(":");
1919
- for (const excludedScope of exclusions.commandScopes) {
1920
- if (excludedScope === "") return true;
1921
- const spaceTokens = excludedScope.split(" ");
1922
- if (tokens.slice(0, spaceTokens.length).join(" ") === excludedScope) return true;
1923
- const colonTokens = excludedScope.split(":");
1924
- if (tokens.slice(0, colonTokens.length).join(":") === excludedScope) return true;
1925
- }
1926
- return false;
1927
- }
1928
- /**
1929
- * Regex matching {{politty:...}} placeholders.
1930
- * NOTE: only use with String.match / String.replace, never with .exec in a loop,
1931
- * because the /g flag makes the regex stateful via lastIndex.
1932
- */
1933
- const TEMPLATE_PLACEHOLDER_REGEX = /\{\{politty:[^{}]*\}\}/g;
1934
- function validateTemplatePlaceholderSyntax(templateContent, templatePath) {
1935
- const validPlaceholderStarts = /* @__PURE__ */ new Set();
1936
- for (const match of templateContent.matchAll(TEMPLATE_PLACEHOLDER_REGEX)) {
1937
- const start = match.index;
1938
- const end = start + match[0].length;
1939
- if (templateContent[start - 1] === "{" || templateContent[end] === "}") {
1940
- const snippet = templateContent.slice(Math.max(0, start - 1), Math.min(templateContent.length, end + 1)).split("\n")[0];
1941
- throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
1942
- }
1943
- validPlaceholderStarts.add(start);
1944
- }
1945
- let searchIndex = 0;
1946
- while (true) {
1947
- const placeholderStart = templateContent.indexOf("{{politty:", searchIndex);
1948
- if (placeholderStart === -1) return;
1949
- if (!validPlaceholderStarts.has(placeholderStart)) {
1950
- const snippet = templateContent.slice(placeholderStart, placeholderStart + 80).split("\n")[0];
1951
- throw new Error(`Malformed politty placeholder in template "${templatePath}": "${snippet}". Expected {{politty:...}}.`);
1952
- }
1953
- searchIndex = placeholderStart + 10;
1954
- }
1955
- }
1956
- function getUnknownSectionTypeError(scope, allCommands) {
1957
- const separatorIndex = scope.lastIndexOf(":");
1958
- if (separatorIndex === -1) return null;
1959
- const commandScope = scope.slice(0, separatorIndex);
1960
- const sectionType = scope.slice(separatorIndex + 1);
1961
- if (sectionType === "" || !allCommands.has(commandScope)) return null;
1962
- return `Unknown section type "${sectionType}" for command scope "${formatCommandPath(commandScope)}". Valid section types: ${SECTION_TYPES.join(", ")}`;
1963
- }
1964
- /**
1965
- * Extract a marker section from content
1966
- * Returns the content between start and end markers (including markers)
1967
- */
1968
- function extractMarkerSection(content, startMarker, endMarker) {
1969
- const startIndex = content.indexOf(startMarker);
1970
- if (startIndex === -1) return null;
1971
- const endIndex = content.indexOf(endMarker, startIndex);
1972
- if (endIndex === -1) return null;
1973
- return content.slice(startIndex, endIndex + endMarker.length);
1974
- }
1975
- /**
1976
- * Replace a marker section in content
1977
- * Returns the updated content with the new section
1978
- */
1979
- function replaceMarkerSection(content, startMarker, endMarker, newSection) {
1980
- const startIndex = content.indexOf(startMarker);
1981
- if (startIndex === -1) return null;
1982
- const endIndex = content.indexOf(endMarker, startIndex);
1983
- if (endIndex === -1) return null;
1984
- return content.slice(0, startIndex) + newSection + content.slice(endIndex + endMarker.length);
1985
- }
1986
- /**
1987
- * Check if config is the { args, options? } shape (not shorthand ArgsShape)
1988
- *
1989
- * Distinguishes between:
1990
- * - { args: ArgsShape, options?: ArgsTableOptions } → returns true
1991
- * - ArgsShape (e.g., { verbose: ZodType, args: ZodType }) → returns false
1992
- *
1993
- * The key insight is that in the { args, options? } shape, config.args is an ArgsShape
1994
- * (Record of ZodTypes), while in shorthand, config itself is the ArgsShape and config.args
1995
- * would be a single ZodType if user has an option named "args".
1996
- */
1997
- function isGlobalOptionsConfigWithOptions(config) {
1998
- if (typeof config !== "object" || config === null || !("args" in config)) return false;
1999
- return !(config.args instanceof zod.z.ZodType);
2000
- }
2001
- /**
2002
- * Collect option fields that are actually rendered by global options markers.
2003
- * Positional args are not rendered in args tables, so they must not be excluded.
2004
- */
2005
- function collectRenderableGlobalOptionFields(argsShape) {
2006
- return require_schema_extractor.extractFields(zod.z.object(argsShape)).fields.filter((field) => !field.positional);
2007
- }
2008
- /**
2009
- * Compare option definitions for global-options compatibility.
2010
- */
2011
- function areGlobalOptionsEquivalent(a, b) {
2012
- const { schema: _aSchema, ...aRest } = a;
2013
- const { schema: _bSchema, ...bRest } = b;
2014
- return (0, node_util.isDeepStrictEqual)(aRest, bRest);
2015
- }
2016
- /**
2017
- * Normalize rootDoc.globalOptions to { args, options? } form.
2018
- */
2019
- function normalizeGlobalOptions(config) {
2020
- if (!config) return void 0;
2021
- return isGlobalOptionsConfigWithOptions(config) ? config : { args: config };
2022
- }
2023
- /**
2024
- * Derive an ArgsShape from a globalArgs Zod schema, retaining only non-positional option fields.
2025
- * Returns undefined when globalArgs is undefined or contains no option fields.
2026
- * Used to build globalOptionDefinitions from globalArgs when rootDoc is not available.
2027
- */
2028
- function deriveGlobalArgsShape(globalArgs) {
2029
- if (!globalArgs) return void 0;
2030
- const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
2031
- if (optionFields.length === 0) return void 0;
2032
- return Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
2033
- }
2034
- /**
2035
- * Collect global option definitions from rootDoc.
2036
- * Global options are intentionally applied to all generated command sections.
2037
- */
2038
- function collectGlobalOptionDefinitions(rootDoc) {
2039
- const globalOptions = /* @__PURE__ */ new Map();
2040
- if (!rootDoc?.globalOptions) return globalOptions;
2041
- const normalized = normalizeGlobalOptions(rootDoc.globalOptions);
2042
- if (!normalized) return globalOptions;
2043
- for (const field of collectRenderableGlobalOptionFields(normalized.args)) globalOptions.set(field.name, field);
2044
- return globalOptions;
2045
- }
2046
- /**
2047
- * Derive CommandCategory[] from files mapping.
2048
- * Category title/description come from the first command in each file entry.
2049
- */
2050
- function deriveIndexFromFiles(files, rootDocPath, allCommands, ignores) {
2051
- const categories = [];
2052
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
2053
- const { commandPaths, topLevelCommands } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2054
- if (commandPaths.length === 0) continue;
2055
- const docPath = "./" + node_path.relative(node_path.dirname(rootDocPath), filePath).replace(/\\/g, "/");
2056
- const firstCmdPath = commandPaths[0];
2057
- const cmdInfo = firstCmdPath !== void 0 ? allCommands.get(firstCmdPath) : void 0;
2058
- const fileConfig = Array.isArray(fileConfigRaw) ? void 0 : fileConfigRaw;
2059
- categories.push({
2060
- title: fileConfig?.title ?? cmdInfo?.name ?? node_path.basename(filePath, node_path.extname(filePath)),
2061
- description: fileConfig?.description ?? cmdInfo?.description ?? "",
2062
- commands: topLevelCommands,
2063
- allowedCommands: commandPaths,
2064
- docPath
2065
- });
2066
- }
2067
- return categories;
2068
- }
2069
- /**
2070
- * Build index categories for the {{politty:index}} placeholder from other template outputs.
2071
- * Each category lists exactly the heading-producing scopes of that output (noExpand), so the
2072
- * index never links to commands that template mode did not render.
2073
- */
2074
- function deriveIndexFromTemplateOutputs(templateMeta, currentOutputPath, indexFilePath, allCommands) {
2075
- const normalizedCurrent = normalizeDocPathForComparison(currentOutputPath);
2076
- const categories = [];
2077
- for (const [outputPath, meta] of templateMeta.entries()) {
2078
- if (normalizeDocPathForComparison(outputPath) === normalizedCurrent) continue;
2079
- const scopes = meta.headingScopes;
2080
- if (scopes.length === 0) continue;
2081
- const docPath = "./" + node_path.relative(node_path.dirname(indexFilePath), outputPath).replace(/\\/g, "/");
2082
- const firstScope = scopes[0];
2083
- const cmdInfo = firstScope !== void 0 ? allCommands.get(firstScope) : void 0;
2084
- categories.push({
2085
- title: meta.indexTitle ?? cmdInfo?.name ?? node_path.basename(outputPath, node_path.extname(outputPath)),
2086
- description: meta.indexDescription ?? cmdInfo?.description ?? "",
2087
- commands: scopes,
2088
- docPath,
2089
- noExpand: true
2090
- });
2091
- }
2092
- return categories;
2093
- }
2094
- /**
2095
- * Collect command paths that are actually documented in configured files.
2096
- */
2097
- function collectDocumentedCommandPaths(files, allCommands, ignores) {
2098
- const documentedCommandPaths = /* @__PURE__ */ new Set();
2099
- for (const fileConfigRaw of Object.values(files)) {
2100
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2101
- for (const commandPath of commandPaths) documentedCommandPaths.add(commandPath);
2102
- }
2103
- return documentedCommandPaths;
2104
- }
2105
- /**
2106
- * Collect command paths that are targeted in configured files.
2107
- */
2108
- function collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) {
2109
- const documentedTargetCommandPaths = /* @__PURE__ */ new Set();
2110
- for (const filePath of Object.keys(files)) {
2111
- const targetCommandsInFile = findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores);
2112
- for (const commandPath of targetCommandsInFile) documentedTargetCommandPaths.add(commandPath);
2113
- }
2114
- return documentedTargetCommandPaths;
2115
- }
2116
- function commandPathMatchesTarget(commandPath, targetCommands) {
2117
- return targetCommands.some((targetCommand) => isSubcommandOf(commandPath, targetCommand));
2118
- }
2119
- function templateMetaReferencesCommandTarget(meta, targetCommands) {
2120
- return meta.referencedScopes.some((scope) => commandPathMatchesTarget(scope, targetCommands));
2121
- }
2122
- function templateMetaShouldProcessForTarget(meta, targetCommands) {
2123
- return meta.emitsIndex || meta.emitsGlobalOptions || templateMetaReferencesCommandTarget(meta, targetCommands);
2124
- }
2125
- /**
2126
- * Validate that excluded command options match globalOptions definitions.
2127
- */
2128
- function validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptions) {
2129
- if (globalOptions.size === 0) return;
2130
- const conflicts = [];
2131
- for (const commandPath of documentedCommandPaths) {
2132
- const info = allCommands.get(commandPath);
2133
- if (!info) continue;
2134
- for (const option of info.options) {
2135
- const globalOption = globalOptions.get(option.name);
2136
- if (!globalOption) continue;
2137
- if (!areGlobalOptionsEquivalent(globalOption, option)) conflicts.push(`Command "${formatCommandPath(commandPath)}" option "--${option.cliName}" does not match globalOptions definition for "${option.name}".`);
2138
- }
2139
- }
2140
- if (conflicts.length > 0) throw new Error(`Invalid globalOptions configuration:\n - ${conflicts.join("\n - ")}`);
2141
- }
2142
- /**
2143
- * Build global options content (anchor + args table) without markers
2144
- */
2145
- function buildGlobalOptionsContent(config) {
2146
- return ["<a id=\"global-options\"></a>", renderArgsTable(config.args, config.options)].join("\n");
2147
- }
2148
- /**
2149
- * Generate global options section content with markers
2150
- */
2151
- function generateGlobalOptionsSection(config) {
2152
- return [
2153
- globalOptionsStartMarker(),
2154
- buildGlobalOptionsContent(config),
2155
- globalOptionsEndMarker()
2156
- ].join("\n");
2157
- }
2158
- /**
2159
- * Generate index section content with markers
2160
- */
2161
- async function generateIndexSection(categories, command, scope, options) {
2162
- const startMarker = indexStartMarker(scope);
2163
- const endMarker = indexEndMarker(scope);
2164
- return [
2165
- startMarker,
2166
- await renderCommandIndex(command, categories, options),
2167
- endMarker
2168
- ].join("\n");
2169
- }
2170
- /**
2171
- * Normalize a doc file path for equivalence checks.
2172
- */
2173
- function normalizeDocPathForComparison(filePath) {
2174
- return node_path.resolve(filePath);
2175
- }
2176
- /**
2177
- * Process global options marker in file content
2178
- * Returns result with updated content and any diffs
2179
- */
2180
- async function processGlobalOptionsMarker(existingContent, globalOptionsConfig, updateMode, formatter, autoInsertIfMissing) {
2181
- let content = existingContent;
2182
- const diffs = [];
2183
- let hasError = false;
2184
- let wasUpdated = false;
2185
- const startMarker = globalOptionsStartMarker();
2186
- const endMarker = globalOptionsEndMarker();
2187
- const generatedSection = await applyFormatter(generateGlobalOptionsSection(globalOptionsConfig), formatter);
2188
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
2189
- if (!existingSection) {
2190
- if (updateMode && autoInsertIfMissing) {
2191
- content = content.trimEnd() + "\n\n" + generatedSection + "\n";
2192
- wasUpdated = true;
2193
- return {
2194
- content,
2195
- diffs,
2196
- hasError,
2197
- wasUpdated
2198
- };
2199
- }
2200
- hasError = true;
2201
- diffs.push(`Global options marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
2202
- return {
2203
- content,
2204
- diffs,
2205
- hasError,
2206
- wasUpdated
2207
- };
2208
- }
2209
- if (existingSection !== generatedSection) if (updateMode) {
2210
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
2211
- if (updated) {
2212
- content = updated;
2213
- wasUpdated = true;
2214
- } else {
2215
- hasError = true;
2216
- diffs.push("Failed to replace global options section");
2217
- }
2218
- } else {
2219
- hasError = true;
2220
- diffs.push(formatDiff(existingSection, generatedSection));
2221
- }
2222
- return {
2223
- content,
2224
- diffs,
2225
- hasError,
2226
- wasUpdated
2227
- };
2228
- }
2229
- /**
2230
- * Process a static content marker (root-header or root-footer).
2231
- * Inserts/updates the marker section with the given content.
2232
- */
2233
- async function processStaticMarker(existingContent, markerLabel, startMarker, endMarker, rawContent, updateMode, formatter, autoInsertIfMissing) {
2234
- let content = existingContent;
2235
- const diffs = [];
2236
- let hasError = false;
2237
- let wasUpdated = false;
2238
- const generatedSection = [
2239
- startMarker,
2240
- await applyFormatter(rawContent, formatter),
2241
- endMarker
2242
- ].join("\n");
2243
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
2244
- if (!existingSection) {
2245
- if (updateMode && autoInsertIfMissing) {
2246
- content = content.trimEnd() + "\n\n" + generatedSection + "\n";
2247
- wasUpdated = true;
2248
- return {
2249
- content,
2250
- diffs,
2251
- hasError,
2252
- wasUpdated
2253
- };
2254
- }
2255
- hasError = true;
2256
- diffs.push(`${markerLabel} marker not found in file. Expected markers:\n${startMarker}\n...\n${endMarker}`);
2257
- return {
2258
- content,
2259
- diffs,
2260
- hasError,
2261
- wasUpdated
2262
- };
2263
- }
2264
- if (existingSection !== generatedSection) if (updateMode) {
2265
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
2266
- if (updated) {
2267
- content = updated;
2268
- wasUpdated = true;
2269
- } else {
2270
- hasError = true;
2271
- diffs.push(`Failed to replace ${markerLabel} section`);
2272
- }
2273
- } else {
2274
- hasError = true;
2275
- diffs.push(formatDiff(existingSection, generatedSection));
2276
- }
2277
- return {
2278
- content,
2279
- diffs,
2280
- hasError,
2281
- wasUpdated
2282
- };
2283
- }
2284
- /**
2285
- * Process index marker in file content
2286
- * Returns result with updated content and any diffs.
2287
- * If the marker is not present in the file, the section is silently skipped.
2288
- */
2289
- async function processIndexMarker(existingContent, categories, command, scope, updateMode, formatter, indexOptions) {
2290
- let content = existingContent;
2291
- const diffs = [];
2292
- let hasError = false;
2293
- let wasUpdated = false;
2294
- const startMarker = indexStartMarker(scope);
2295
- const endMarker = indexEndMarker(scope);
2296
- const hasStartMarker = content.includes(startMarker);
2297
- const hasEndMarker = content.includes(endMarker);
2298
- if (!hasStartMarker && !hasEndMarker) return {
2299
- content,
2300
- diffs,
2301
- hasError,
2302
- wasUpdated
2303
- };
2304
- if (!hasStartMarker || !hasEndMarker) {
2305
- hasError = true;
2306
- diffs.push("Index marker section is malformed: both start and end markers are required.");
2307
- return {
2308
- content,
2309
- diffs,
2310
- hasError,
2311
- wasUpdated
2312
- };
2313
- }
2314
- const existingSection = extractMarkerSection(content, startMarker, endMarker);
2315
- if (!existingSection) {
2316
- hasError = true;
2317
- diffs.push("Index marker section is malformed: start marker must appear before end marker.");
2318
- return {
2319
- content,
2320
- diffs,
2321
- hasError,
2322
- wasUpdated
2323
- };
2324
- }
2325
- const generatedSection = await applyFormatter(await generateIndexSection(categories, command, scope, indexOptions), formatter);
2326
- if (existingSection !== generatedSection) if (updateMode) {
2327
- const updated = replaceMarkerSection(content, startMarker, endMarker, generatedSection);
2328
- if (updated) {
2329
- content = updated;
2330
- wasUpdated = true;
2331
- } else {
2332
- hasError = true;
2333
- diffs.push("Failed to replace index section");
2334
- }
2335
- } else {
2336
- hasError = true;
2337
- diffs.push(formatDiff(existingSection, generatedSection));
2338
- }
2339
- return {
2340
- content,
2341
- diffs,
2342
- hasError,
2343
- wasUpdated
2344
- };
2345
- }
2346
- /**
2347
- * Find which file contains a specific command
2348
- */
2349
- function findFileForCommand(commandPath, files, allCommands, ignores) {
2350
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
2351
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2352
- if (commandPaths.includes(commandPath)) return filePath;
2353
- }
2354
- return null;
2355
- }
2356
- /**
2357
- * Find which target commands are contained in a file
2358
- * Also expands each target command to include subcommands that are NOT explicitly in specifiedCommands
2359
- */
2360
- function findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) {
2361
- const fileConfigRaw = files[filePath];
2362
- if (!fileConfigRaw) return [];
2363
- const { specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2364
- const expandedTargets = /* @__PURE__ */ new Set();
2365
- for (const targetCmd of targetCommands) {
2366
- if (!commandPaths.includes(targetCmd)) continue;
2367
- expandedTargets.add(targetCmd);
2368
- for (const cmdPath of commandPaths) if (isSubcommandOf(cmdPath, targetCmd) && !specifiedCommands.includes(cmdPath)) expandedTargets.add(cmdPath);
2369
- }
2370
- return Array.from(expandedTargets);
2371
- }
2372
- /**
2373
- * Generate a single command section (already contains section markers from renderer)
2374
- */
2375
- function generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores = [], excludeOptionNames, templateExclusions) {
2376
- const info = allCommands.get(cmdPath);
2377
- if (!info) return null;
2378
- if (templateExclusions && isCommandScopeExcluded(info.commandPath, templateExclusions.commandScopes)) return null;
2379
- const enriched = {
2380
- ...info,
2381
- filePath,
2382
- fileMap,
2383
- rootDocPath
2384
- };
2385
- if (ignores.length > 0 || templateExclusions && templateExclusions.commandScopes.size > 0) enriched.subCommands = info.subCommands.filter((sub) => {
2386
- const subCommandPath = sub.fullPath.join(" ");
2387
- if (ignores.some((pattern) => matchesIgnorePattern(subCommandPath, pattern))) return false;
2388
- return !(templateExclusions && isCommandScopeExcluded(subCommandPath, templateExclusions.commandScopes));
2389
- });
2390
- if (hasGlobalOptions !== void 0) enriched.hasGlobalOptions = hasGlobalOptions;
2391
- if (excludeOptionNames && excludeOptionNames.size > 0) {
2392
- enriched.options = info.options.filter((opt) => !excludeOptionNames.has(opt.name));
2393
- if (info.extracted) enriched.extracted = filterExtractedFields(info.extracted, excludeOptionNames);
2394
- }
2395
- let rendered = render(enriched);
2396
- if (templateExclusions) for (const [scope, sectionTypes] of templateExclusions.commandSections) {
2397
- if (scope !== info.commandPath) continue;
2398
- for (const sectionType of sectionTypes) {
2399
- const section = extractSectionMarker(rendered, sectionType, scope);
2400
- if (section !== null) rendered = rendered.replace(section, "");
2401
- }
2402
- rendered = collapseBlankLinesOutsideCodeFences(rendered);
2403
- }
2404
- return rendered;
2405
- }
2406
- function generateCommandTreeMarkdown(cmdPath, allCommands, render, ignores, filePath, fileMap, rootDocPath, hasGlobalOptions, excludeOptionNames, templateExclusions) {
2407
- const commandPaths = getTemplateCommandTreePaths(cmdPath, allCommands, ignores, templateExclusions);
2408
- const sections = [];
2409
- for (const commandPath of commandPaths) {
2410
- const section = generateCommandSection(commandPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores, excludeOptionNames, templateExclusions);
2411
- if (section !== null) sections.push(section);
2412
- }
2413
- return sections.length === 0 ? null : sections.join("\n");
2414
- }
2415
- /**
2416
- * Return a copy of ExtractedFields with the named options removed from every field collection
2417
- * (top-level fields, union options, and discriminated-union variants). Used to exclude global
2418
- * options from grouped option tables rendered directly from `extracted`.
2419
- */
2420
- function filterExtractedFields(extracted, excludeOptionNames) {
2421
- const result = {
2422
- ...extracted,
2423
- fields: extracted.fields.filter((f) => !excludeOptionNames.has(f.name))
2424
- };
2425
- if (extracted.unionOptions) result.unionOptions = extracted.unionOptions.map((opt) => filterExtractedFields(opt, excludeOptionNames));
2426
- if (extracted.variants) result.variants = extracted.variants.map((variant) => ({
2427
- ...variant,
2428
- fields: variant.fields.filter((f) => !excludeOptionNames.has(f.name))
2429
- }));
2430
- return result;
2431
- }
2432
- /**
2433
- * Generate markdown for a file containing multiple commands
2434
- * Each command section is wrapped with markers for partial validation
2435
- */
2436
- function generateFileMarkdown(commandPaths, allCommands, render, filePath, fileMap, specifiedOrder, fileConfig, rootDocPath, hasGlobalOptions, ignores = []) {
2437
- const sections = [];
2438
- const header = fileConfig ? generateFileHeader(fileConfig) : null;
2439
- if (header) sections.push(header);
2440
- const sortedPaths = sortDepthFirst(commandPaths, specifiedOrder ?? []);
2441
- for (const cmdPath of sortedPaths) {
2442
- const section = generateCommandSection(cmdPath, allCommands, render, filePath, fileMap, rootDocPath, hasGlobalOptions, ignores);
2443
- if (section) sections.push(section);
2444
- }
2445
- return `${sections.join("\n")}\n`;
2446
- }
2447
- /**
2448
- * Build a map of command path to file path
2449
- */
2450
- function buildFileMap(files, allCommands, ignores) {
2451
- const fileMap = {};
2452
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
2453
- const { commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2454
- for (const cmdPath of commandPaths) setFileMapEntry(fileMap, cmdPath, filePath);
2455
- }
2456
- return fileMap;
2457
- }
2458
- /**
2459
- * Execute examples for commands based on configuration
2460
- */
2461
- async function executeConfiguredExamples(allCommands, examplesConfig, rootCommand) {
2462
- for (const [cmdPath, cmdConfig] of Object.entries(examplesConfig)) {
2463
- const commandInfo = allCommands.get(cmdPath);
2464
- if (!commandInfo?.examples?.length) continue;
2465
- const config = cmdConfig === true ? {} : cmdConfig;
2466
- const commandPath = cmdPath ? cmdPath.split(" ") : [];
2467
- commandInfo.exampleResults = await executeExamples(commandInfo.examples, config, rootCommand, commandPath);
2468
- }
2469
- }
2470
- /**
2471
- * Convert PathConfig to FileMapping with explicit command paths.
2472
- * Uses noExpand to prevent subcommand expansion since paths are pre-resolved.
2473
- */
2474
- function pathToFiles(pathConfig, allCommands) {
2475
- if (typeof pathConfig === "string") return {
2476
- files: { [pathConfig]: Array.from(allCommands.keys()) },
2477
- rootDocPath: pathConfig
2478
- };
2479
- const { root, commands = {} } = pathConfig;
2480
- const files = {};
2481
- const assignedToOtherFiles = /* @__PURE__ */ new Set();
2482
- const sortedEntries = Object.entries(commands).sort(([a], [b]) => b.split(" ").length - a.split(" ").length);
2483
- for (const [cmdPath, filePath] of sortedEntries) {
2484
- if (!files[filePath]) files[filePath] = {
2485
- commands: [],
2486
- noExpand: true
2487
- };
2488
- const fc = files[filePath];
2489
- for (const existingPath of allCommands.keys()) if ((existingPath === cmdPath || existingPath.startsWith(cmdPath + " ")) && !assignedToOtherFiles.has(existingPath)) {
2490
- fc.commands.push(existingPath);
2491
- assignedToOtherFiles.add(existingPath);
2492
- }
2493
- }
2494
- files[root] = {
2495
- commands: Array.from(allCommands.keys()).filter((p) => !assignedToOtherFiles.has(p)),
2496
- noExpand: true
2497
- };
2498
- return {
2499
- files,
2500
- rootDocPath: root
2501
- };
2502
- }
2503
- /**
2504
- * Generate documentation from command definition
2505
- */
2506
- async function generateDoc(config) {
2507
- const { command, ignores = [], format = {}, formatter, examples: examplesConfig, targetCommands, globalArgs, customizable = false } = config;
2508
- const allCommands = await collectAllCommands(command);
2509
- let files;
2510
- let usingPathConfig = false;
2511
- let resolvedRootDocPath;
2512
- if (config.path !== void 0) {
2513
- if (config.files !== void 0) throw new Error("Cannot specify both \"path\" and \"files\". Use one or the other.");
2514
- const converted = pathToFiles(config.path, allCommands);
2515
- files = converted.files;
2516
- resolvedRootDocPath = converted.rootDocPath;
2517
- usingPathConfig = true;
2518
- } else if (config.files !== void 0) files = config.files;
2519
- else if (config.templates !== void 0) files = {};
2520
- else throw new Error("Either \"path\", \"files\", or \"templates\" must be specified.");
2521
- let rootDoc = config.rootDoc;
2522
- if (!rootDoc && usingPathConfig && (globalArgs || config.rootInfo)) rootDoc = { path: resolvedRootDocPath };
2523
- if (globalArgs && rootDoc && !rootDoc.globalOptions) {
2524
- const optionFields = require_schema_extractor.extractFields(globalArgs).fields.filter((f) => !f.positional);
2525
- if (optionFields.length > 0) {
2526
- const globalShape = Object.fromEntries(optionFields.map((f) => [f.name, f.schema]));
2527
- rootDoc = {
2528
- ...rootDoc,
2529
- globalOptions: globalShape
2530
- };
2531
- }
2532
- }
2533
- const updateMode = isTruthyEnv(UPDATE_GOLDEN_ENV);
2534
- const doctorMode = isTruthyEnv(DOCTOR_ENV);
2535
- let hasDoctorIssues = false;
2536
- if (rootDoc && !usingPathConfig) {
2537
- const normalizedRootDocPath = normalizeDocPathForComparison(rootDoc.path);
2538
- 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.`);
2539
- }
2540
- if (examplesConfig) await executeConfiguredExamples(allCommands, examplesConfig, command);
2541
- const hasTargetCommands = targetCommands !== void 0 && targetCommands.length > 0;
2542
- const globalOptionDefinitions = collectGlobalOptionDefinitions(rootDoc);
2543
- const templateGlobalOptionFields = /* @__PURE__ */ new Map();
2544
- if (config.templates) if (globalOptionDefinitions.size > 0) for (const [name, field] of globalOptionDefinitions) templateGlobalOptionFields.set(name, field);
2545
- else {
2546
- const shape = deriveGlobalArgsShape(globalArgs);
2547
- if (shape) for (const field of collectRenderableGlobalOptionFields(shape)) templateGlobalOptionFields.set(field.name, field);
2548
- }
2549
- const documentedCommandPaths = hasTargetCommands ? collectTargetDocumentedCommandPaths(targetCommands, files, allCommands, ignores) : collectDocumentedCommandPaths(files, allCommands, ignores);
2550
- const allFilesCommands = [];
2551
- for (const fileConfigRaw of Object.values(files)) {
2552
- const fileConfig = normalizeFileConfig(fileConfigRaw);
2553
- allFilesCommands.push(...fileConfig.commands);
2554
- }
2555
- validateIgnoresExist(ignores, allCommands);
2556
- validateNoConflicts(allFilesCommands, ignores, allCommands);
2557
- const fileMap = buildFileMap(files, allCommands, ignores);
2558
- const templateContents = /* @__PURE__ */ new Map();
2559
- const templateExclusions = /* @__PURE__ */ new Map();
2560
- if (config.templates) for (const [outputPath, templatePath] of Object.entries(config.templates)) {
2561
- const templateContent = readFile(templatePath);
2562
- templateContents.set(outputPath, templateContent);
2563
- if (templateContent !== null) templateExclusions.set(outputPath, buildTemplateExclusions(collectExcludedTemplatePlaceholders(templateContent), allCommands));
2564
- }
2565
- const templateEntries = Object.entries(config.templates ?? {});
2566
- const templateMeta = /* @__PURE__ */ new Map();
2567
- const templateValidationErrors = /* @__PURE__ */ new Map();
2568
- if (templateEntries.length > 0) {
2569
- const normalizedRootDocPath = rootDoc ? normalizeDocPathForComparison(rootDoc.path) : null;
2570
- const normalizedFileKeys = new Set(Object.keys(files).map(normalizeDocPathForComparison));
2571
- const normalizedTemplateOutputs = /* @__PURE__ */ new Set();
2572
- const allNormalizedTemplateOutputs = new Set(templateEntries.map(([outputPath]) => normalizeDocPathForComparison(outputPath)));
2573
- for (const [outputPath, templatePath] of templateEntries) {
2574
- const normalizedOutput = normalizeDocPathForComparison(outputPath);
2575
- const normalizedSource = normalizeDocPathForComparison(templatePath);
2576
- if (normalizedFileKeys.has(normalizedOutput)) throw new Error(`Template output path "${outputPath}" conflicts with an existing files key.`);
2577
- if (normalizedRootDocPath && normalizedOutput === normalizedRootDocPath) throw new Error(`Template output path "${outputPath}" conflicts with rootDoc.path "${rootDoc.path}".`);
2578
- if (normalizedTemplateOutputs.has(normalizedOutput)) throw new Error(`Duplicate template output path: "${outputPath}".`);
2579
- normalizedTemplateOutputs.add(normalizedOutput);
2580
- if (normalizedSource === normalizedOutput) throw new Error(`Template output path "${outputPath}" must not be the same as its source template path.`);
2581
- if (normalizedFileKeys.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a files output key.`);
2582
- if (normalizedRootDocPath && normalizedSource === normalizedRootDocPath) throw new Error(`Template source path "${templatePath}" conflicts with rootDoc.path "${rootDoc.path}".`);
2583
- if (allNormalizedTemplateOutputs.has(normalizedSource)) throw new Error(`Template source path "${templatePath}" conflicts with a template output path.`);
2584
- }
2585
- const availableCommandPaths = Array.from(allCommands.keys()).join(", ");
2586
- for (const [outputPath, templatePath] of templateEntries) {
2587
- const templateContent = templateContents.get(outputPath) ?? null;
2588
- const validationErrors = [];
2589
- if (templateContent === null) {
2590
- templateMeta.set(outputPath, {
2591
- referencedScopes: [],
2592
- headingScopes: [],
2593
- commandTreeRoots: [],
2594
- emitsGlobalOptions: false,
2595
- emitsIndex: false
2596
- });
2597
- templateValidationErrors.set(outputPath, validationErrors);
2598
- continue;
2599
- }
2600
- try {
2601
- validateTemplatePlaceholderSyntax(templateContent, templatePath);
2602
- } catch (error) {
2603
- validationErrors.push(error instanceof Error ? error.message : String(error));
2604
- }
2605
- const placeholders = Array.from(new Set(templateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
2606
- const scopes = /* @__PURE__ */ new Set();
2607
- const headingScopes = /* @__PURE__ */ new Set();
2608
- const commandTreeRoots = /* @__PURE__ */ new Set();
2609
- let emitsGlobalOptions = false;
2610
- let emitsIndex = false;
2611
- const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
2612
- const indexMetadata = collectTemplateIndexMetadata(templateContent);
2613
- for (const placeholder of placeholders) {
2614
- const placeholderKey = templatePlaceholderKey(placeholder);
2615
- if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) continue;
2616
- const parsed = parsePlaceholder(placeholder, allCommands);
2617
- if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) continue;
2618
- if (parsed.kind === "invalid") {
2619
- validationErrors.push(`${parsed.reason} (in template "${templatePath}")`);
2620
- continue;
2621
- }
2622
- if (parsed.kind === "command") {
2623
- const { scope, type } = parsed;
2624
- if (!allCommands.has(scope)) {
2625
- const sectionTypeError = getUnknownSectionTypeError(scope, allCommands);
2626
- if (sectionTypeError) {
2627
- validationErrors.push(`${sectionTypeError} (in template "${templatePath}")`);
2628
- continue;
2629
- }
2630
- validationErrors.push(`Unknown command scope "${scope}" in template "${templatePath}". Available: ${availableCommandPaths}`);
2631
- continue;
2632
- }
2633
- if (ignores.some((pattern) => matchesIgnorePattern(scope, pattern))) {
2634
- validationErrors.push(`Command scope "${scope}" in template "${templatePath}" conflicts with ignores configuration.`);
2635
- continue;
2636
- }
2637
- if (type === void 0) {
2638
- const commandTreePaths = getTemplateCommandTreePaths(scope, allCommands, ignores, exclusions);
2639
- if (!isCommandSectionExcluded(scope, "heading", exclusions)) commandTreeRoots.add(scope);
2640
- for (const commandTreePath of commandTreePaths) {
2641
- scopes.add(commandTreePath);
2642
- if (!isCommandSectionExcluded(commandTreePath, "heading", exclusions)) headingScopes.add(commandTreePath);
2643
- }
2644
- } else {
2645
- scopes.add(scope);
2646
- if (type === "heading" && !isCommandSectionExcluded(scope, "heading", exclusions)) {
2647
- headingScopes.add(scope);
2648
- commandTreeRoots.add(scope);
2649
- }
2650
- }
2651
- } else if (parsed.kind === "global-options") emitsGlobalOptions = true;
2652
- else if (parsed.kind === "index") emitsIndex = true;
2653
- }
2654
- if (emitsGlobalOptions) {
2655
- if (!(!!rootDoc?.globalOptions || deriveGlobalArgsShape(globalArgs) !== void 0)) validationErrors.push(`Template "${templatePath}" uses {{politty:global-options}} but no global options are configured (neither rootDoc.globalOptions nor globalArgs with non-positional options).`);
2656
- }
2657
- templateMeta.set(outputPath, {
2658
- referencedScopes: Array.from(scopes),
2659
- headingScopes: Array.from(headingScopes),
2660
- commandTreeRoots: Array.from(commandTreeRoots),
2661
- emitsGlobalOptions,
2662
- emitsIndex,
2663
- ...indexMetadata.title !== void 0 ? { indexTitle: indexMetadata.title } : {},
2664
- ...indexMetadata.description !== void 0 ? { indexDescription: indexMetadata.description } : {}
2665
- });
2666
- templateValidationErrors.set(outputPath, validationErrors);
2667
- }
2668
- for (const meta of templateMeta.values()) {
2669
- if (hasTargetCommands && !templateMetaShouldProcessForTarget(meta, targetCommands)) continue;
2670
- for (const scope of meta.referencedScopes) documentedCommandPaths.add(scope);
2671
- }
2672
- }
2673
- if (hasTargetCommands) for (const targetCommand of targetCommands) {
2674
- const targetFilePath = findFileForCommand(targetCommand, files, allCommands, ignores);
2675
- const targetTemplatePath = Array.from(templateMeta.values()).some((meta) => templateMetaReferencesCommandTarget(meta, [targetCommand]));
2676
- if (!targetFilePath && !targetTemplatePath) throw new Error(`Target command "${targetCommand}" not found in any file or template configuration`);
2677
- }
2678
- const activeTemplateMeta = hasTargetCommands && config.templates ? new Map(Array.from(templateMeta.entries()).filter(([, meta]) => templateMetaShouldProcessForTarget(meta, targetCommands))) : templateMeta;
2679
- for (const [outputPath, validationErrors] of templateValidationErrors.entries()) if (validationErrors.length > 0 && activeTemplateMeta.has(outputPath)) throw new Error(validationErrors.join("\n"));
2680
- const templateGlobalOptionsProviderPaths = Array.from(templateMeta.entries()).filter(([, meta]) => meta.emitsGlobalOptions).map(([outputPath]) => outputPath);
2681
- const templateGlobalOptionsProviderPath = templateGlobalOptionsProviderPaths.length === 1 ? templateGlobalOptionsProviderPaths[0] : void 0;
2682
- validateGlobalOptionCompatibility(documentedCommandPaths, allCommands, globalOptionDefinitions);
2683
- if (globalOptionDefinitions.size === 0 && templateGlobalOptionFields.size > 0) {
2684
- const emittingTemplateScopes = /* @__PURE__ */ new Set();
2685
- for (const meta of activeTemplateMeta.values()) {
2686
- if (!meta.emitsGlobalOptions && templateGlobalOptionsProviderPath === void 0) continue;
2687
- for (const scope of meta.referencedScopes) emittingTemplateScopes.add(scope);
2688
- }
2689
- validateGlobalOptionCompatibility(emittingTemplateScopes, allCommands, templateGlobalOptionFields);
2690
- }
2691
- if (globalOptionDefinitions.size > 0) for (const info of allCommands.values()) {
2692
- info.options = info.options.filter((opt) => !globalOptionDefinitions.has(opt.name));
2693
- if (info.extracted) info.extracted = filterExtractedFields(info.extracted, new Set(globalOptionDefinitions.keys()));
2694
- }
2695
- const templateFileMap = {};
2696
- for (const [scope, outputPath] of Object.entries(fileMap)) setFileMapEntry(templateFileMap, scope, outputPath);
2697
- const scopeRootLength = (root) => root === "" ? 0 : root.split(" ").length;
2698
- const templateOwners = /* @__PURE__ */ new Map();
2699
- for (const [templateOutputPath, meta] of templateMeta.entries()) for (const scope of meta.headingScopes) {
2700
- if (Object.prototype.hasOwnProperty.call(fileMap, scope)) continue;
2701
- let bestRootLen = -1;
2702
- for (const root of meta.commandTreeRoots) if (isSubcommandOf(scope, root)) bestRootLen = Math.max(bestRootLen, scopeRootLength(root));
2703
- if (bestRootLen < 0) continue;
2704
- const existing = templateOwners.get(scope);
2705
- if (!existing || bestRootLen > existing.rootLen) templateOwners.set(scope, {
2706
- outputPath: templateOutputPath,
2707
- rootLen: bestRootLen
2708
- });
2709
- }
2710
- for (const [scope, { outputPath }] of templateOwners) setFileMapEntry(templateFileMap, scope, outputPath);
2711
- const results = [];
2712
- let hasError = false;
2713
- for (const [filePath, fileConfigRaw] of Object.entries(files)) {
2714
- const { fileConfig, specifiedCommands, commandPaths } = resolveConfiguredCommandPaths(fileConfigRaw, allCommands, ignores);
2715
- if (specifiedCommands.length === 0) continue;
2716
- if (commandPaths.length === 0) continue;
2717
- const fileTargetCommands = hasTargetCommands ? findTargetCommandsInFile(targetCommands, filePath, files, allCommands, ignores) : [];
2718
- if (hasTargetCommands && fileTargetCommands.length === 0) continue;
2719
- let fileStatus = "match";
2720
- const diffs = [];
2721
- const minDepth = Math.min(...commandPaths.map((p) => allCommands.get(p)?.depth ?? 1));
2722
- const adjustedHeadingLevel = Math.max(1, (format?.headingLevel ?? 1) - (minDepth - 1));
2723
- const isRootDocFile = usingPathConfig && rootDoc && normalizeDocPathForComparison(filePath) === normalizeDocPathForComparison(rootDoc.path);
2724
- const fileUsesMarkers = usingPathConfig || customizable;
2725
- const fileRenderer = createCommandRenderer({
2726
- ...format,
2727
- headingLevel: adjustedHeadingLevel,
2728
- markerless: !fileUsesMarkers
2729
- });
2730
- const render = fileConfig.render ?? fileRenderer;
2731
- if (Boolean(isRootDocFile) || hasTargetCommands && fileUsesMarkers) {
2732
- let existingContent = readFile(filePath);
2733
- const sortedCommandPaths = sortDepthFirst(commandPaths, specifiedCommands);
2734
- const effectiveTargetCommands = hasTargetCommands ? fileTargetCommands : commandPaths;
2735
- for (const targetCommand of effectiveTargetCommands) {
2736
- const rawSection = generateCommandSection(targetCommand, allCommands, render, filePath, templateFileMap, rootDoc?.path, globalOptionDefinitions.size > 0, ignores);
2737
- if (!rawSection) throw new Error(`Target command "${targetCommand}" not found in commands`);
2738
- const generatedSection = await applyFormatter(rawSection, formatter);
2739
- if (!existingContent) {
2740
- if (updateMode) {
2741
- const header = targetCommand === "" && fileConfig ? generateFileHeader(fileConfig) : null;
2742
- const fullContent = header ? `${header}\n${generatedSection}` : generatedSection;
2743
- writeFile(filePath, fullContent);
2744
- existingContent = fullContent;
2745
- fileStatus = "created";
2746
- } else {
2747
- hasError = true;
2748
- fileStatus = "diff";
2749
- diffs.push(`File does not exist. Target command "${targetCommand}" section cannot be validated.`);
2750
- }
2751
- continue;
2752
- }
2753
- const existingMarkers = collectSectionMarkers(existingContent, targetCommand);
2754
- if (existingMarkers.length === 0) {
2755
- if (updateMode) {
2756
- existingContent = insertCommandSections(existingContent, targetCommand, generatedSection, sortedCommandPaths);
2757
- writeFile(filePath, existingContent);
2758
- if (fileStatus !== "created") fileStatus = "updated";
2759
- } else {
2760
- hasError = true;
2761
- fileStatus = "diff";
2762
- diffs.push(`Existing file does not contain section markers for command "${targetCommand}"`);
2763
- }
2764
- continue;
2765
- }
2766
- for (const sectionType of existingMarkers) {
2767
- const existingSection = extractSectionMarker(existingContent, sectionType, targetCommand);
2768
- const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
2769
- if (!existingSection) continue;
2770
- if (!generatedSectionPart) {
2771
- const emptyMarker = sectionStartMarker(sectionType, targetCommand) + "\n" + sectionEndMarker(sectionType, targetCommand);
2772
- if (existingSection !== emptyMarker) if (updateMode) {
2773
- const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, emptyMarker);
2774
- if (!updated) throw new Error(`Failed to replace stale ${sectionType} section for command "${targetCommand}"`);
2775
- existingContent = updated.replace(/\n{3,}/g, "\n\n");
2776
- writeFile(filePath, existingContent);
2777
- if (fileStatus !== "created") fileStatus = "updated";
2778
- } else {
2779
- hasError = true;
2780
- fileStatus = "diff";
2781
- diffs.push(formatDiff(existingSection, emptyMarker));
2782
- }
2783
- continue;
2784
- }
2785
- if (existingSection !== generatedSectionPart) if (updateMode) {
2786
- const updated = replaceSectionMarker(existingContent, sectionType, targetCommand, generatedSectionPart);
2787
- if (updated) {
2788
- existingContent = updated;
2789
- writeFile(filePath, existingContent);
2790
- if (fileStatus !== "created") fileStatus = "updated";
2791
- } else throw new Error(`Failed to replace ${sectionType} section for command "${targetCommand}"`);
2792
- } else {
2793
- hasError = true;
2794
- fileStatus = "diff";
2795
- diffs.push(formatDiff(existingSection, generatedSectionPart));
2796
- }
2797
- }
2798
- if (doctorMode || customizable) {
2799
- const generatedMarkers = collectSectionMarkers(generatedSection, targetCommand);
2800
- const existingMarkerSet = new Set(existingMarkers);
2801
- for (const sectionType of generatedMarkers) {
2802
- if (existingMarkerSet.has(sectionType)) continue;
2803
- const generatedSectionPart = extractSectionMarker(generatedSection, sectionType, targetCommand);
2804
- if (!generatedSectionPart) continue;
2805
- if (doctorMode && updateMode) {
2806
- existingContent = insertSectionMarkerAtOrder(existingContent, sectionType, targetCommand, generatedSectionPart);
2807
- writeFile(filePath, existingContent);
2808
- if (fileStatus !== "created") fileStatus = "updated";
2809
- } else if (doctorMode) {
2810
- hasError = true;
2811
- hasDoctorIssues = true;
2812
- fileStatus = "diff";
2813
- diffs.push(`[doctor] Missing section marker "${sectionType}" for command "${formatCommandPath(targetCommand)}". Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert.\n${generatedSectionPart}`);
2814
- } else console.warn(`[politty] Missing "${sectionType}" section for command "${formatCommandPath(targetCommand)}" in ${filePath}. Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to insert it, or leave it removed to opt that section out.`);
2815
- }
2816
- }
2817
- }
2818
- if (existingContent) {
2819
- const existingMarkerPaths = collectSectionMarkerPaths(existingContent);
2820
- const commandPathSet = new Set(commandPaths);
2821
- if (updateMode) {
2822
- let removedAny = false;
2823
- for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
2824
- existingContent = removeCommandSections(existingContent, markerPath);
2825
- removedAny = true;
2826
- }
2827
- if (removedAny) {
2828
- writeFile(filePath, existingContent);
2829
- if (fileStatus !== "created") fileStatus = "updated";
2830
- }
2831
- } else for (const markerPath of existingMarkerPaths) if (!commandPathSet.has(markerPath)) {
2832
- hasError = true;
2833
- fileStatus = "diff";
2834
- diffs.push(`Found orphaned section markers for deleted command "${formatCommandPath(markerPath)}"`);
2835
- }
2836
- }
2837
- } else {
2838
- const generatedMarkdown = await applyFormatter(generateFileMarkdown(commandPaths, allCommands, render, filePath, templateFileMap, specifiedCommands, fileConfig, rootDoc?.path, globalOptionDefinitions.size > 0, ignores), formatter);
2839
- const comparison = compareWithExisting(generatedMarkdown, filePath);
2840
- if (comparison.match) {} else if (updateMode) {
2841
- writeFile(filePath, generatedMarkdown);
2842
- fileStatus = comparison.fileExists ? "updated" : "created";
2843
- } else {
2844
- hasError = true;
2845
- fileStatus = "diff";
2846
- if (comparison.diff) diffs.push(comparison.diff);
2847
- }
2848
- }
2849
- if (diffs.length > 0) fileStatus = "diff";
2850
- results.push({
2851
- path: filePath,
2852
- status: fileStatus,
2853
- diff: diffs.length > 0 ? diffs.join("\n\n") : void 0
2854
- });
2855
- }
2856
- let normalizedTemplateGlobalOptions;
2857
- if (rootDoc?.globalOptions) normalizedTemplateGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
2858
- else {
2859
- const shape = deriveGlobalArgsShape(globalArgs);
2860
- if (shape) normalizedTemplateGlobalOptions = { args: shape };
2861
- }
2862
- for (const [outputPath, templatePath] of templateEntries) {
2863
- if (!activeTemplateMeta.has(outputPath)) continue;
2864
- const templateContent = templateContents.get(outputPath) ?? null;
2865
- if (templateContent === null) {
2866
- hasError = true;
2867
- results.push({
2868
- path: outputPath,
2869
- status: "diff",
2870
- diff: `Template file not found: ${templatePath}`
2871
- });
2872
- continue;
2873
- }
2874
- const meta = templateMeta.get(outputPath);
2875
- const templateLineEnding = detectLineEnding(templateContent);
2876
- const outputTemplateContent = stripPolittyFrontMatterForOutput(templateContent);
2877
- const headingDepths = (meta?.headingScopes ?? []).map((s) => allCommands.get(s)?.depth ?? 1);
2878
- const minDepth = headingDepths.length > 0 ? Math.min(...headingDepths) : 1;
2879
- const adjustedHeadingLevel = clampHeadingLevel((format?.headingLevel ?? 1) - (minDepth - 1));
2880
- const templateRenderer = createCommandRenderer({
2881
- ...format,
2882
- headingLevel: adjustedHeadingLevel
2883
- });
2884
- const outputEmitsGlobalOptions = meta?.emitsGlobalOptions ?? false;
2885
- const excludeOptionNames = (rootDoc !== void 0 && globalOptionDefinitions.size > 0 || outputEmitsGlobalOptions || templateGlobalOptionsProviderPath !== void 0) && templateGlobalOptionFields.size > 0 ? new Set(templateGlobalOptionFields.keys()) : void 0;
2886
- const sectionHasGlobalOptions = excludeOptionNames !== void 0;
2887
- const effectiveRootDocPath = outputEmitsGlobalOptions ? outputPath : rootDoc?.path ?? templateGlobalOptionsProviderPath;
2888
- const placeholders = Array.from(new Set(outputTemplateContent.match(TEMPLATE_PLACEHOLDER_REGEX) ?? []));
2889
- const replacements = /* @__PURE__ */ new Map();
2890
- const exclusions = templateExclusions.get(outputPath) ?? createTemplateExclusions(/* @__PURE__ */ new Set());
2891
- for (const placeholder of placeholders) {
2892
- const placeholderKey = templatePlaceholderKey(placeholder);
2893
- if (exclusions.rawKeys.has(placeholderKey) || isRawCommandPlaceholderUnderExcludedScope(placeholderKey, exclusions)) {
2894
- replacements.set(placeholder, "");
2895
- continue;
2896
- }
2897
- const parsed = parsePlaceholder(placeholder, allCommands);
2898
- if (shouldSkipTemplatePlaceholder(placeholder, parsed, exclusions)) {
2899
- replacements.set(placeholder, "");
2900
- continue;
2901
- }
2902
- if (parsed.kind === "invalid") throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}": ${parsed.reason}`);
2903
- if (parsed.kind === "command") {
2904
- const { scope, type } = parsed;
2905
- if (type === void 0) {
2906
- const rawSection = generateCommandTreeMarkdown(scope, allCommands, templateRenderer, ignores, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, excludeOptionNames, exclusions);
2907
- if (rawSection === null) {
2908
- replacements.set(placeholder, "");
2909
- continue;
2910
- }
2911
- replacements.set(placeholder, stripPolittyMarkers(rawSection));
2912
- } else {
2913
- const rawSection = generateCommandSection(scope, allCommands, templateRenderer, outputPath, templateFileMap, effectiveRootDocPath, sectionHasGlobalOptions, ignores, excludeOptionNames, exclusions);
2914
- if (rawSection === null) {
2915
- replacements.set(placeholder, "");
2916
- continue;
2917
- }
2918
- const extracted = extractSectionMarker(rawSection, type, scope);
2919
- replacements.set(placeholder, extracted === null ? "" : stripPolittyMarkers(extracted));
2920
- }
2921
- } else if (parsed.kind === "global-options") if (normalizedTemplateGlobalOptions) replacements.set(placeholder, buildGlobalOptionsContent(normalizedTemplateGlobalOptions));
2922
- else replacements.set(placeholder, "");
2923
- else if (parsed.kind === "index") {
2924
- const indexContent = await renderCommandIndex(command, [...deriveIndexFromFiles(files, outputPath, allCommands, ignores), ...deriveIndexFromTemplateOutputs(templateMeta, outputPath, outputPath, allCommands)], rootDoc?.index);
2925
- replacements.set(placeholder, indexContent);
2926
- }
2927
- }
2928
- let generated = outputTemplateContent.replace(/((?:\r?\n)*)([ \t]*)(\{\{politty:[^{}]*\}\})([ \t]*)((?:\r?\n)*)/g, (match, leadNl, leadWs, placeholder, trailWs, trailNl, offset, fullString) => {
2929
- const replacement = replacements.get(placeholder);
2930
- if (replacement === void 0) throw new Error(`Internal error: unresolved placeholder "${placeholder}" in template "${templatePath}".`);
2931
- const startsLine = leadNl !== "" || offset === 0 || fullString[offset - 1] === "\n";
2932
- const endsLine = trailNl !== "" || offset + match.length === fullString.length;
2933
- if (replacement === "" && startsLine && endsLine) {
2934
- if (leadNl === "" || trailNl === "") return "";
2935
- const leadBreaks = countLineBreaks(leadNl);
2936
- const trailBreaks = countLineBreaks(trailNl);
2937
- const widest = Math.max(leadBreaks, trailBreaks);
2938
- const lineEnding = leadBreaks >= trailBreaks ? detectLineEnding(leadNl) : detectLineEnding(trailNl);
2939
- return widest >= 2 ? lineEnding + lineEnding : widest === 1 ? lineEnding : "";
2940
- }
2941
- return `${leadNl}${leadWs}${replacement}${trailWs}${trailNl}`;
2942
- });
2943
- generated = `${generated.trimEnd()}${templateLineEnding}`;
2944
- generated = await applyFormatter(generated, formatter);
2945
- const comparison = compareWithExisting(generated, outputPath);
2946
- let templateStatus = "match";
2947
- let templateDiff;
2948
- if (comparison.match) {} else if (updateMode) {
2949
- writeFile(outputPath, generated);
2950
- templateStatus = comparison.fileExists ? "updated" : "created";
2951
- } else {
2952
- hasError = true;
2953
- templateStatus = "diff";
2954
- if (comparison.diff) templateDiff = comparison.diff;
2955
- }
2956
- results.push({
2957
- path: outputPath,
2958
- status: templateStatus,
2959
- diff: templateDiff
2960
- });
2961
- }
2962
- if (rootDoc) {
2963
- const rootDocFilePath = rootDoc.path;
2964
- let rootDocStatus = "match";
2965
- const rootDocDiffs = [];
2966
- const existingContent = readFile(rootDocFilePath);
2967
- if (existingContent === null) {
2968
- hasError = true;
2969
- rootDocStatus = "diff";
2970
- rootDocDiffs.push("File does not exist. Cannot validate rootDoc markers.");
2971
- } else {
2972
- let content = existingContent;
2973
- let markerUpdated = false;
2974
- const rootInfo = config.rootInfo;
2975
- const rootDocFileConfig = { title: rootInfo?.title ?? command.name };
2976
- if (rootDoc.headingLevel !== void 0) rootDocFileConfig.headingLevel = rootDoc.headingLevel;
2977
- const rootDescription = rootInfo?.description ?? command.description;
2978
- if (rootDescription !== void 0) rootDocFileConfig.description = rootDescription;
2979
- const headerResult = processFileHeader(content, rootDocFileConfig, updateMode);
2980
- content = headerResult.content;
2981
- if (headerResult.diff) rootDocDiffs.push(headerResult.diff);
2982
- if (headerResult.hasError) hasError = true;
2983
- if (headerResult.wasUpdated) markerUpdated = true;
2984
- if (rootInfo?.header) {
2985
- const headerMarkerResult = await processStaticMarker(content, "Root header", rootHeaderStartMarker(), rootHeaderEndMarker(), rootInfo.header, updateMode, formatter, usingPathConfig);
2986
- content = headerMarkerResult.content;
2987
- rootDocDiffs.push(...headerMarkerResult.diffs);
2988
- if (headerMarkerResult.hasError) hasError = true;
2989
- if (headerMarkerResult.wasUpdated) markerUpdated = true;
2990
- }
2991
- if (!usingPathConfig) {
2992
- const unexpectedSectionPaths = collectSectionMarkerPaths(content);
2993
- if (unexpectedSectionPaths.length > 0) if (updateMode) {
2994
- for (const commandPath of unexpectedSectionPaths) content = removeCommandSections(content, commandPath);
2995
- markerUpdated = true;
2996
- } else {
2997
- hasError = true;
2998
- rootDocDiffs.push(`Found unexpected section markers in rootDoc: ${unexpectedSectionPaths.map((commandPath) => `"${formatCommandPath(commandPath)}"`).join(", ")}.`);
2999
- }
3000
- }
3001
- const normalizedGlobalOptions = normalizeGlobalOptions(rootDoc.globalOptions);
3002
- if (normalizedGlobalOptions) {
3003
- const globalOptionsResult = await processGlobalOptionsMarker(content, normalizedGlobalOptions, updateMode, formatter, usingPathConfig);
3004
- content = globalOptionsResult.content;
3005
- rootDocDiffs.push(...globalOptionsResult.diffs);
3006
- if (globalOptionsResult.hasError) hasError = true;
3007
- if (globalOptionsResult.wasUpdated) markerUpdated = true;
3008
- }
3009
- const derivedCategories = deriveIndexFromFiles(files, rootDocFilePath, allCommands, ignores);
3010
- const indexScope = node_path.relative(process.cwd(), rootDocFilePath).replace(/\\/g, "/");
3011
- const indexResult = await processIndexMarker(content, derivedCategories, command, indexScope, updateMode, formatter, rootDoc.index);
3012
- content = indexResult.content;
3013
- rootDocDiffs.push(...indexResult.diffs);
3014
- if (indexResult.hasError) hasError = true;
3015
- if (indexResult.wasUpdated) markerUpdated = true;
3016
- if (rootInfo?.footer) {
3017
- const footerMarkerResult = await processStaticMarker(content, "Root footer", rootFooterStartMarker(), rootFooterEndMarker(), rootInfo.footer, updateMode, formatter, usingPathConfig);
3018
- content = footerMarkerResult.content;
3019
- rootDocDiffs.push(...footerMarkerResult.diffs);
3020
- if (footerMarkerResult.hasError) hasError = true;
3021
- if (footerMarkerResult.wasUpdated) markerUpdated = true;
3022
- }
3023
- if (updateMode && markerUpdated) {
3024
- writeFile(rootDocFilePath, content);
3025
- if (rootDocStatus === "match") rootDocStatus = "updated";
3026
- }
3027
- }
3028
- if (rootDocDiffs.length > 0) rootDocStatus = "diff";
3029
- results.push({
3030
- path: rootDocFilePath,
3031
- status: rootDocStatus,
3032
- diff: rootDocDiffs.length > 0 ? rootDocDiffs.join("\n\n") : void 0
3033
- });
3034
- }
3035
- const errorHint = hasDoctorIssues ? `Run with ${DOCTOR_ENV}=true ${UPDATE_GOLDEN_ENV}=true to fix missing markers.` : `Run with ${UPDATE_GOLDEN_ENV}=true to update.`;
3036
- return {
3037
- success: !hasError,
3038
- files: results,
3039
- error: hasError ? `Documentation is out of date. ${errorHint}` : void 0
3040
- };
3041
- }
3042
- /**
3043
- * Assert that documentation matches golden files
3044
- * Throws an error if there are differences and update mode is not enabled
3045
- */
3046
- async function assertDocMatch(config) {
3047
- const result = await generateDoc(config);
3048
- if (!result.success) {
3049
- const diffMessages = result.files.filter((f) => f.status === "diff").map((f) => {
3050
- let msg = `File: ${f.path}\n`;
3051
- if (f.diff) msg += f.diff;
3052
- return msg;
3053
- }).join("\n\n");
3054
- 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.`));
3055
- }
3056
- }
3057
- /**
3058
- * Initialize documentation files by deleting them
3059
- * Only deletes when update mode is enabled (POLITTY_DOCS_UPDATE=true)
3060
- * Use this in beforeAll to ensure skipped tests don't leave stale sections
3061
- * @param config - Config containing files to initialize, or a single file path
3062
- * @param fileSystem - Optional fs implementation (useful when fs is mocked)
3063
- */
3064
- function initDocFile(config, fileSystem) {
3065
- if (!isTruthyEnv("POLITTY_DOCS_UPDATE")) return;
3066
- if (typeof config === "string") deleteFile(config, fileSystem);
3067
- else {
3068
- const protectedPaths = new Set(Object.values(config.templates ?? {}).map(normalizeDocPathForComparison));
3069
- if (config.rootDoc) protectedPaths.add(normalizeDocPathForComparison(config.rootDoc.path));
3070
- const isProtectedPath = (p) => protectedPaths.has(normalizeDocPathForComparison(p));
3071
- if (config.files) for (const filePath of Object.keys(config.files)) {
3072
- if (isProtectedPath(filePath)) continue;
3073
- deleteFile(filePath, fileSystem);
3074
- }
3075
- if (config.templates) for (const outputPath of Object.keys(config.templates)) {
3076
- if (isProtectedPath(outputPath)) continue;
3077
- deleteFile(outputPath, fileSystem);
3078
- }
3079
- }
3080
- }
3081
-
3082
- //#endregion
3083
- exports.DOCTOR_ENV = DOCTOR_ENV;
3084
- exports.GLOBAL_OPTIONS_MARKER_PREFIX = GLOBAL_OPTIONS_MARKER_PREFIX;
3085
- exports.INDEX_MARKER_PREFIX = INDEX_MARKER_PREFIX;
3086
- exports.ROOT_FOOTER_MARKER_PREFIX = ROOT_FOOTER_MARKER_PREFIX;
3087
- exports.ROOT_HEADER_MARKER_PREFIX = ROOT_HEADER_MARKER_PREFIX;
3088
- exports.SECTION_MARKER_PREFIX = SECTION_MARKER_PREFIX;
3089
- exports.SECTION_TYPES = SECTION_TYPES;
3090
- exports.UPDATE_GOLDEN_ENV = UPDATE_GOLDEN_ENV;
3091
- exports.assertDocMatch = assertDocMatch;
3092
- exports.buildCommandInfo = buildCommandInfo;
3093
- exports.collectAllCommands = collectAllCommands;
3094
- exports.compareWithExisting = compareWithExisting;
3095
- exports.createCommandRenderer = createCommandRenderer;
3096
- exports.defaultRenderers = defaultRenderers;
3097
- exports.executeExamples = executeExamples;
3098
- exports.formatDiff = formatDiff;
3099
- exports.generateDoc = generateDoc;
3100
- exports.globalOptionsEndMarker = globalOptionsEndMarker;
3101
- exports.globalOptionsStartMarker = globalOptionsStartMarker;
3102
- exports.indexEndMarker = indexEndMarker;
3103
- exports.indexStartMarker = indexStartMarker;
3104
- exports.initDocFile = initDocFile;
3105
- exports.renderArgsTable = renderArgsTable;
3106
- exports.renderArgumentsList = renderArgumentsList;
3107
- exports.renderArgumentsListFromArray = renderArgumentsListFromArray;
3108
- exports.renderArgumentsTable = renderArgumentsTable;
3109
- exports.renderArgumentsTableFromArray = renderArgumentsTableFromArray;
3110
- exports.renderCommandIndex = renderCommandIndex;
3111
- exports.renderExamplesDefault = renderExamplesDefault;
3112
- exports.renderOptionsList = renderOptionsList;
3113
- exports.renderOptionsListFromArray = renderOptionsListFromArray;
3114
- exports.renderOptionsTable = renderOptionsTable;
3115
- exports.renderOptionsTableFromArray = renderOptionsTableFromArray;
3116
- exports.renderSubcommandsTable = renderSubcommandsTable;
3117
- exports.renderSubcommandsTableFromArray = renderSubcommandsTableFromArray;
3118
- exports.renderUsage = renderUsage;
3119
- exports.resolveLazyCommand = require_schema_extractor.resolveLazyCommand;
3120
- exports.rootFooterEndMarker = rootFooterEndMarker;
3121
- exports.rootFooterStartMarker = rootFooterStartMarker;
3122
- exports.rootHeaderEndMarker = rootHeaderEndMarker;
3123
- exports.rootHeaderStartMarker = rootHeaderStartMarker;
3124
- exports.sectionEndMarker = sectionEndMarker;
3125
- exports.sectionStartMarker = sectionStartMarker;
3126
- exports.writeFile = writeFile;
3127
- //# sourceMappingURL=index.cjs.map