padrone 1.1.0 → 1.2.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 (80) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  4. package/dist/args-CKNh7Dm9.mjs +175 -0
  5. package/dist/args-CKNh7Dm9.mjs.map +1 -0
  6. package/dist/chunk-y_GBKt04.mjs +5 -0
  7. package/dist/codegen/index.d.mts +305 -0
  8. package/dist/codegen/index.d.mts.map +1 -0
  9. package/dist/codegen/index.mjs +1348 -0
  10. package/dist/codegen/index.mjs.map +1 -0
  11. package/dist/completion.d.mts +64 -0
  12. package/dist/completion.d.mts.map +1 -0
  13. package/dist/completion.mjs +417 -0
  14. package/dist/completion.mjs.map +1 -0
  15. package/dist/docs/index.d.mts +34 -0
  16. package/dist/docs/index.d.mts.map +1 -0
  17. package/dist/docs/index.mjs +404 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-Dvx7jFXr.d.mts +82 -0
  20. package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
  21. package/dist/help-mUIX0T0V.mjs +1195 -0
  22. package/dist/help-mUIX0T0V.mjs.map +1 -0
  23. package/dist/index.d.mts +120 -546
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1180 -1197
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/test.d.mts +112 -0
  28. package/dist/test.d.mts.map +1 -0
  29. package/dist/test.mjs +138 -0
  30. package/dist/test.mjs.map +1 -0
  31. package/dist/types-qrtt0135.d.mts +1037 -0
  32. package/dist/types-qrtt0135.d.mts.map +1 -0
  33. package/dist/update-check-EbNDkzyV.mjs +146 -0
  34. package/dist/update-check-EbNDkzyV.mjs.map +1 -0
  35. package/package.json +61 -21
  36. package/src/args.ts +365 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +312 -0
  40. package/src/cli/index.ts +159 -0
  41. package/src/cli/init.ts +135 -0
  42. package/src/cli/link.ts +320 -0
  43. package/src/cli/wrap.ts +152 -0
  44. package/src/codegen/README.md +118 -0
  45. package/src/codegen/code-builder.ts +226 -0
  46. package/src/codegen/discovery.ts +232 -0
  47. package/src/codegen/file-emitter.ts +73 -0
  48. package/src/codegen/generators/barrel-file.ts +16 -0
  49. package/src/codegen/generators/command-file.ts +184 -0
  50. package/src/codegen/generators/command-tree.ts +124 -0
  51. package/src/codegen/index.ts +33 -0
  52. package/src/codegen/parsers/fish.ts +163 -0
  53. package/src/codegen/parsers/help.ts +378 -0
  54. package/src/codegen/parsers/merge.ts +158 -0
  55. package/src/codegen/parsers/zsh.ts +221 -0
  56. package/src/codegen/schema-to-code.ts +199 -0
  57. package/src/codegen/template.ts +69 -0
  58. package/src/codegen/types.ts +143 -0
  59. package/src/colorizer.ts +2 -2
  60. package/src/command-utils.ts +501 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1036 -305
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +149 -63
  66. package/src/help.ts +151 -55
  67. package/src/index.ts +12 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +31 -16
  70. package/src/repl-loop.ts +317 -0
  71. package/src/runtime.ts +304 -0
  72. package/src/shell-utils.ts +83 -0
  73. package/src/test.ts +285 -0
  74. package/src/type-helpers.ts +10 -10
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +752 -154
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +44 -40
  79. package/src/zod.d.ts +2 -2
  80. package/src/options.ts +0 -180
@@ -0,0 +1,607 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { commandSymbol } from '../command-utils.ts';
4
+ import type { HelpArgumentInfo, HelpInfo, HelpPositionalInfo, HelpSubcommandInfo } from '../formatter.ts';
5
+ import { getHelpInfo } from '../help.ts';
6
+ import type { AnyPadroneCommand } from '../types.ts';
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ export type DocsFormat = 'markdown' | 'html' | 'man' | 'json';
13
+
14
+ export type DocsOptions = {
15
+ /** Output format. Defaults to 'markdown'. */
16
+ format?: DocsFormat;
17
+ /** Output directory. If not set, docs are returned but not written. */
18
+ output?: string;
19
+ /** Include hidden commands and options. Defaults to false. */
20
+ includeHidden?: boolean;
21
+ /** Frontmatter generator for markdown files (VitePress, Starlight, etc.). */
22
+ frontmatter?: (info: HelpInfo, depth: number) => Record<string, unknown>;
23
+ /** Whether to overwrite existing files. Defaults to true. */
24
+ overwrite?: boolean;
25
+ /** Print what would be written without writing. */
26
+ dryRun?: boolean;
27
+ };
28
+
29
+ export type DocsPage = {
30
+ /** File path relative to output directory (e.g., "deploy.md", "index.md"). */
31
+ path: string;
32
+ /** Generated content for this page. */
33
+ content: string;
34
+ /** The command name this page documents. */
35
+ command: string;
36
+ };
37
+
38
+ export type DocsResult = {
39
+ /** All generated pages. */
40
+ pages: DocsPage[];
41
+ /** Files that were written (empty if no output dir). */
42
+ written: string[];
43
+ /** Files that were skipped (already exist, no overwrite). */
44
+ skipped: string[];
45
+ /** Files that failed to write. */
46
+ errors: { file: string; error: Error }[];
47
+ };
48
+
49
+ // ============================================================================
50
+ // Help Info Collection
51
+ // ============================================================================
52
+
53
+ function collectAllHelpInfo(cmd: AnyPadroneCommand, includeHidden: boolean): HelpInfo[] {
54
+ const info = getHelpInfo(cmd, 'standard');
55
+ const result: HelpInfo[] = [info];
56
+
57
+ if (cmd.commands) {
58
+ for (const sub of cmd.commands) {
59
+ if (!includeHidden && sub.hidden) continue;
60
+ result.push(...collectAllHelpInfo(sub, includeHidden));
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Markdown Generator
69
+ // ============================================================================
70
+
71
+ function generateFrontmatter(data: Record<string, unknown>): string {
72
+ const lines: string[] = ['---'];
73
+ for (const [key, value] of Object.entries(data)) {
74
+ if (typeof value === 'string') {
75
+ lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
76
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
77
+ lines.push(`${key}: ${value}`);
78
+ } else if (Array.isArray(value)) {
79
+ lines.push(`${key}:`);
80
+ for (const item of value) {
81
+ lines.push(` - "${String(item).replace(/"/g, '\\"')}"`);
82
+ }
83
+ }
84
+ }
85
+ lines.push('---');
86
+ return lines.join('\n');
87
+ }
88
+
89
+ function formatMarkdownPositional(arg: HelpPositionalInfo): string {
90
+ const parts: string[] = [];
91
+ parts.push(`- \`${arg.name}\``);
92
+ if (arg.type) parts.push(`*(${arg.type})*`);
93
+ if (arg.optional) parts.push('*(optional)*');
94
+ if (arg.default !== undefined) parts.push(`— default: \`${String(arg.default)}\``);
95
+ if (arg.description) parts.push(`— ${arg.description}`);
96
+ return parts.join(' ');
97
+ }
98
+
99
+ function formatMarkdownArgument(arg: HelpArgumentInfo): string[] {
100
+ const lines: string[] = [];
101
+
102
+ const flagName = arg.negatable ? `--[no-]${arg.name}` : `--${arg.name}`;
103
+ const aliases = arg.aliases?.length ? `${arg.aliases.map((a) => `-${a}`).join(', ')}, ` : '';
104
+ const header = `#### \`${aliases}${flagName}\``;
105
+ lines.push(header);
106
+ lines.push('');
107
+
108
+ if (arg.description) {
109
+ lines.push(arg.description);
110
+ lines.push('');
111
+ }
112
+
113
+ const meta: string[] = [];
114
+ if (arg.type) meta.push(`**Type:** \`${arg.type}\``);
115
+ if (arg.optional) meta.push('**Optional**');
116
+ else meta.push('**Required**');
117
+ if (arg.default !== undefined) meta.push(`**Default:** \`${String(arg.default)}\``);
118
+ if (arg.enum) meta.push(`**Choices:** ${arg.enum.map((v) => `\`${v}\``).join(', ')}`);
119
+ if (arg.variadic) meta.push('**Repeatable**');
120
+ if (arg.deprecated) {
121
+ const msg = typeof arg.deprecated === 'string' ? arg.deprecated : '';
122
+ meta.push(`**Deprecated**${msg ? `: ${msg}` : ''}`);
123
+ }
124
+
125
+ if (meta.length > 0) {
126
+ lines.push(meta.join(' | '));
127
+ lines.push('');
128
+ }
129
+
130
+ if (arg.env) {
131
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
132
+ lines.push(`**Environment:** ${envVars.map((v) => `\`${v}\``).join(', ')}`);
133
+ lines.push('');
134
+ }
135
+
136
+ if (arg.configKey) {
137
+ lines.push(`**Config key:** \`${arg.configKey}\``);
138
+ lines.push('');
139
+ }
140
+
141
+ if (arg.examples?.length) {
142
+ lines.push(`**Examples:** ${arg.examples.map((e) => `\`${typeof e === 'string' ? e : JSON.stringify(e)}\``).join(', ')}`);
143
+ lines.push('');
144
+ }
145
+
146
+ return lines;
147
+ }
148
+
149
+ function formatMarkdownSubcommand(sub: HelpSubcommandInfo): string {
150
+ const parts: string[] = [];
151
+ const suffix = sub.hasSubcommands ? ' ...' : '';
152
+ parts.push(`| \`${sub.name}${suffix}\``);
153
+
154
+ const aliases = sub.aliases?.filter((a) => a !== '[default]');
155
+ parts.push(`| ${aliases?.length ? aliases.map((a) => `\`${a}\``).join(', ') : ''}`);
156
+
157
+ const desc = sub.title ?? sub.description ?? '';
158
+ parts.push(`| ${desc}`);
159
+ parts.push('|');
160
+
161
+ return parts.join(' ');
162
+ }
163
+
164
+ function generateMarkdownPage(info: HelpInfo, depth: number, frontmatterFn?: DocsOptions['frontmatter']): string {
165
+ const lines: string[] = [];
166
+
167
+ if (frontmatterFn) {
168
+ const fm = frontmatterFn(info, depth);
169
+ if (Object.keys(fm).length > 0) {
170
+ lines.push(generateFrontmatter(fm));
171
+ lines.push('');
172
+ }
173
+ }
174
+
175
+ // Title
176
+ const displayName = info.name === '<root>' || !info.name ? 'CLI Reference' : info.name;
177
+ lines.push(`# ${displayName}`);
178
+ lines.push('');
179
+
180
+ // Deprecation warning
181
+ if (info.deprecated) {
182
+ const msg = typeof info.deprecated === 'string' ? info.deprecated : 'This command is deprecated.';
183
+ lines.push(`> **Deprecated:** ${msg}`);
184
+ lines.push('');
185
+ }
186
+
187
+ // Description
188
+ if (info.title) {
189
+ lines.push(`> ${info.title}`);
190
+ lines.push('');
191
+ }
192
+ if (info.description) {
193
+ lines.push(info.description);
194
+ lines.push('');
195
+ }
196
+
197
+ // Aliases
198
+ if (info.aliases?.length) {
199
+ const realAliases = info.aliases.filter((a) => a !== '[default]');
200
+ if (realAliases.length > 0) {
201
+ lines.push(`**Aliases:** ${realAliases.map((a) => `\`${a}\``).join(', ')}`);
202
+ lines.push('');
203
+ }
204
+ }
205
+
206
+ // Usage
207
+ const usageParts: string[] = [info.usage.command];
208
+ if (info.usage.hasSubcommands) usageParts.push('[command]');
209
+ if (info.positionals?.length) {
210
+ for (const arg of info.positionals) {
211
+ usageParts.push(arg.optional ? `[${arg.name}]` : `<${arg.name}>`);
212
+ }
213
+ }
214
+ if (info.usage.hasArguments) usageParts.push('[options]');
215
+
216
+ lines.push('## Usage');
217
+ lines.push('');
218
+ lines.push('```');
219
+ lines.push(usageParts.join(' '));
220
+ lines.push('```');
221
+ lines.push('');
222
+
223
+ // Subcommands
224
+ if (info.subcommands?.length) {
225
+ const visibleSubs = info.subcommands.filter((s) => !s.hidden);
226
+ if (visibleSubs.length > 0) {
227
+ lines.push('## Commands');
228
+ lines.push('');
229
+ lines.push('| Command | Aliases | Description |');
230
+ lines.push('| --- | --- | --- |');
231
+ for (const sub of visibleSubs) {
232
+ lines.push(formatMarkdownSubcommand(sub));
233
+ }
234
+ lines.push('');
235
+ }
236
+ }
237
+
238
+ // Positional arguments
239
+ if (info.positionals?.length) {
240
+ lines.push('## Arguments');
241
+ lines.push('');
242
+ for (const arg of info.positionals) {
243
+ lines.push(formatMarkdownPositional(arg));
244
+ }
245
+ lines.push('');
246
+ }
247
+
248
+ // Options
249
+ if (info.arguments?.length) {
250
+ lines.push('## Options');
251
+ lines.push('');
252
+ for (const arg of info.arguments) {
253
+ lines.push(...formatMarkdownArgument(arg));
254
+ }
255
+ }
256
+
257
+ return `${lines.join('\n').trimEnd()}\n`;
258
+ }
259
+
260
+ // ============================================================================
261
+ // HTML Generator
262
+ // ============================================================================
263
+
264
+ function escapeHtml(text: string): string {
265
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
266
+ }
267
+
268
+ function generateHtmlPage(info: HelpInfo, depth: number): string {
269
+ const displayName = info.name === '<root>' || !info.name ? 'CLI Reference' : escapeHtml(info.name);
270
+
271
+ const sections: string[] = [];
272
+
273
+ // Header
274
+ sections.push(`<article class="padrone-docs-page" data-command="${escapeHtml(info.name)}" data-depth="${depth}">`);
275
+ sections.push(` <h1>${displayName}</h1>`);
276
+
277
+ if (info.deprecated) {
278
+ const msg = typeof info.deprecated === 'string' ? escapeHtml(info.deprecated) : 'This command is deprecated.';
279
+ sections.push(` <div class="deprecated-warning"><strong>Deprecated:</strong> ${msg}</div>`);
280
+ }
281
+
282
+ if (info.title) {
283
+ sections.push(` <p class="command-title">${escapeHtml(info.title)}</p>`);
284
+ }
285
+ if (info.description) {
286
+ sections.push(` <p class="command-description">${escapeHtml(info.description)}</p>`);
287
+ }
288
+
289
+ // Aliases
290
+ if (info.aliases?.length) {
291
+ const realAliases = info.aliases.filter((a) => a !== '[default]');
292
+ if (realAliases.length > 0) {
293
+ sections.push(` <p><strong>Aliases:</strong> ${realAliases.map((a) => `<code>${escapeHtml(a)}</code>`).join(', ')}</p>`);
294
+ }
295
+ }
296
+
297
+ // Usage
298
+ const usageParts: string[] = [info.usage.command];
299
+ if (info.usage.hasSubcommands) usageParts.push('[command]');
300
+ if (info.positionals?.length) {
301
+ for (const arg of info.positionals) {
302
+ usageParts.push(arg.optional ? `[${arg.name}]` : `<${arg.name}>`);
303
+ }
304
+ }
305
+ if (info.usage.hasArguments) usageParts.push('[options]');
306
+
307
+ sections.push(' <h2>Usage</h2>');
308
+ sections.push(` <pre><code>${escapeHtml(usageParts.join(' '))}</code></pre>`);
309
+
310
+ // Subcommands
311
+ if (info.subcommands?.length) {
312
+ const visibleSubs = info.subcommands.filter((s) => !s.hidden);
313
+ if (visibleSubs.length > 0) {
314
+ sections.push(' <h2>Commands</h2>');
315
+ sections.push(' <table>');
316
+ sections.push(' <thead><tr><th>Command</th><th>Aliases</th><th>Description</th></tr></thead>');
317
+ sections.push(' <tbody>');
318
+ for (const sub of visibleSubs) {
319
+ const aliases = sub.aliases?.filter((a) => a !== '[default]');
320
+ const desc = sub.title ?? sub.description ?? '';
321
+ const suffix = sub.hasSubcommands ? ' ...' : '';
322
+ sections.push(
323
+ ` <tr><td><code>${escapeHtml(sub.name + suffix)}</code></td><td>${aliases?.length ? aliases.map((a) => `<code>${escapeHtml(a)}</code>`).join(', ') : ''}</td><td>${escapeHtml(desc)}</td></tr>`,
324
+ );
325
+ }
326
+ sections.push(' </tbody>');
327
+ sections.push(' </table>');
328
+ }
329
+ }
330
+
331
+ // Positional arguments
332
+ if (info.positionals?.length) {
333
+ sections.push(' <h2>Arguments</h2>');
334
+ sections.push(' <dl>');
335
+ for (const arg of info.positionals) {
336
+ sections.push(
337
+ ` <dt><code>${escapeHtml(arg.name)}</code>${arg.type ? ` <span class="type">${escapeHtml(arg.type)}</span>` : ''}${arg.optional ? ' <em>(optional)</em>' : ''}</dt>`,
338
+ );
339
+ if (arg.description) sections.push(` <dd>${escapeHtml(arg.description)}</dd>`);
340
+ if (arg.default !== undefined) sections.push(` <dd>Default: <code>${escapeHtml(String(arg.default))}</code></dd>`);
341
+ }
342
+ sections.push(' </dl>');
343
+ }
344
+
345
+ // Options
346
+ if (info.arguments?.length) {
347
+ sections.push(' <h2>Options</h2>');
348
+ sections.push(' <dl>');
349
+ for (const arg of info.arguments) {
350
+ const flagName = arg.negatable ? `--[no-]${arg.name}` : `--${arg.name}`;
351
+ const aliases = arg.aliases?.length ? `${arg.aliases.map((a) => `-${a}`).join(', ')}, ` : '';
352
+ sections.push(
353
+ ` <dt><code>${escapeHtml(aliases + flagName)}</code>${arg.type ? ` <span class="type">${escapeHtml(arg.type)}</span>` : ''}</dt>`,
354
+ );
355
+ if (arg.description) sections.push(` <dd>${escapeHtml(arg.description)}</dd>`);
356
+
357
+ const meta: string[] = [];
358
+ if (arg.optional) meta.push('Optional');
359
+ else meta.push('Required');
360
+ if (arg.default !== undefined) meta.push(`Default: <code>${escapeHtml(String(arg.default))}</code>`);
361
+ if (arg.enum) meta.push(`Choices: ${arg.enum.map((v) => `<code>${escapeHtml(v)}</code>`).join(', ')}`);
362
+ if (arg.variadic) meta.push('Repeatable');
363
+ if (arg.deprecated) {
364
+ const msg = typeof arg.deprecated === 'string' ? escapeHtml(arg.deprecated) : '';
365
+ meta.push(`Deprecated${msg ? `: ${msg}` : ''}`);
366
+ }
367
+ if (meta.length > 0) sections.push(` <dd class="meta">${meta.join(' · ')}</dd>`);
368
+
369
+ if (arg.env) {
370
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
371
+ sections.push(` <dd>Environment: ${envVars.map((v) => `<code>${escapeHtml(v)}</code>`).join(', ')}</dd>`);
372
+ }
373
+ if (arg.configKey) {
374
+ sections.push(` <dd>Config key: <code>${escapeHtml(arg.configKey)}</code></dd>`);
375
+ }
376
+ }
377
+ sections.push(' </dl>');
378
+ }
379
+
380
+ sections.push('</article>');
381
+ return `${sections.join('\n')}\n`;
382
+ }
383
+
384
+ // ============================================================================
385
+ // Man Page Generator
386
+ // ============================================================================
387
+
388
+ function escapeMan(text: string): string {
389
+ return text.replace(/\\/g, '\\\\').replace(/-/g, '\\-').replace(/'/g, '\\(aq');
390
+ }
391
+
392
+ function generateManPage(info: HelpInfo, _depth: number, programName: string): string {
393
+ const commandName = info.name === '<root>' || !info.name ? programName : info.name;
394
+ const manName = commandName.replace(/\s+/g, '-');
395
+ const lines: string[] = [];
396
+
397
+ lines.push(`.TH "${escapeMan(manName.toUpperCase())}" "1" "" "" ""`);
398
+
399
+ // NAME
400
+ lines.push('.SH NAME');
401
+ const desc = info.title ?? info.description ?? '';
402
+ lines.push(`${escapeMan(manName)}${desc ? ` \\- ${escapeMan(desc)}` : ''}`);
403
+
404
+ // SYNOPSIS
405
+ lines.push('.SH SYNOPSIS');
406
+ const usageParts: string[] = [`\\fB${escapeMan(commandName)}\\fR`];
407
+ if (info.usage.hasSubcommands) usageParts.push('[\\fIcommand\\fR]');
408
+ if (info.positionals?.length) {
409
+ for (const arg of info.positionals) {
410
+ usageParts.push(arg.optional ? `[\\fI${escapeMan(arg.name)}\\fR]` : `\\fI${escapeMan(arg.name)}\\fR`);
411
+ }
412
+ }
413
+ if (info.usage.hasArguments) usageParts.push('[\\fIoptions\\fR]');
414
+ lines.push(usageParts.join(' '));
415
+
416
+ // DESCRIPTION
417
+ if (info.description) {
418
+ lines.push('.SH DESCRIPTION');
419
+ lines.push(escapeMan(info.description));
420
+ }
421
+
422
+ // COMMANDS
423
+ if (info.subcommands?.length) {
424
+ const visibleSubs = info.subcommands.filter((s) => !s.hidden);
425
+ if (visibleSubs.length > 0) {
426
+ lines.push('.SH COMMANDS');
427
+ for (const sub of visibleSubs) {
428
+ const suffix = sub.hasSubcommands ? ' ...' : '';
429
+ lines.push(`.TP`);
430
+ lines.push(`\\fB${escapeMan(sub.name + suffix)}\\fR`);
431
+ const subDesc = sub.title ?? sub.description;
432
+ if (subDesc) lines.push(escapeMan(subDesc));
433
+ }
434
+ }
435
+ }
436
+
437
+ // ARGUMENTS
438
+ if (info.positionals?.length) {
439
+ lines.push('.SH ARGUMENTS');
440
+ for (const arg of info.positionals) {
441
+ lines.push('.TP');
442
+ lines.push(`\\fI${escapeMan(arg.name)}\\fR`);
443
+ const parts: string[] = [];
444
+ if (arg.description) parts.push(escapeMan(arg.description));
445
+ if (arg.optional) parts.push('(optional)');
446
+ if (arg.default !== undefined) parts.push(`Default: ${escapeMan(String(arg.default))}`);
447
+ if (parts.length > 0) lines.push(parts.join('. '));
448
+ }
449
+ }
450
+
451
+ // OPTIONS
452
+ if (info.arguments?.length) {
453
+ lines.push('.SH OPTIONS');
454
+ for (const arg of info.arguments) {
455
+ const flagName = arg.negatable ? `\\-\\-[no\\-]${escapeMan(arg.name)}` : `\\-\\-${escapeMan(arg.name)}`;
456
+ const aliases = arg.aliases?.length ? `${arg.aliases.map((a) => `\\-${escapeMan(a)}`).join(', ')}, ` : '';
457
+ lines.push('.TP');
458
+ lines.push(`\\fB${aliases}${flagName}\\fR${arg.type ? ` \\fI${escapeMan(arg.type)}\\fR` : ''}`);
459
+ const parts: string[] = [];
460
+ if (arg.description) parts.push(escapeMan(arg.description));
461
+ if (arg.default !== undefined) parts.push(`Default: ${escapeMan(String(arg.default))}`);
462
+ if (arg.enum) parts.push(`Choices: ${arg.enum.map((v) => escapeMan(v)).join(', ')}`);
463
+ if (parts.length > 0) lines.push(parts.join('. '));
464
+
465
+ if (arg.env) {
466
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
467
+ lines.push(`.br`);
468
+ lines.push(`Environment: ${envVars.map((v) => escapeMan(v)).join(', ')}`);
469
+ }
470
+ }
471
+ }
472
+
473
+ return `${lines.join('\n')}\n`;
474
+ }
475
+
476
+ // ============================================================================
477
+ // Page Path Helpers
478
+ // ============================================================================
479
+
480
+ function commandToPath(info: HelpInfo, ext: string, isRoot: boolean): string {
481
+ if (isRoot) return `index${ext}`;
482
+ // Split on whitespace and replace empty segments (from empty-name default commands) with "_default"
483
+ const segments = info.name.split(/\s+/).map((s) => s || '_default');
484
+ return segments.join('/') + ext;
485
+ }
486
+
487
+ // ============================================================================
488
+ // Index Page Generators
489
+ // ============================================================================
490
+
491
+ function generateMarkdownIndex(rootInfo: HelpInfo, allInfos: HelpInfo[]): string {
492
+ const lines: string[] = [];
493
+ lines.push(`# ${rootInfo.title ?? rootInfo.name ?? 'CLI'} Reference`);
494
+ lines.push('');
495
+
496
+ if (rootInfo.description) {
497
+ lines.push(rootInfo.description);
498
+ lines.push('');
499
+ }
500
+
501
+ if (allInfos.length > 1) {
502
+ lines.push('## Commands');
503
+ lines.push('');
504
+ for (const info of allInfos) {
505
+ const path = commandToPath(info, '.md', info === rootInfo);
506
+ const name = info === rootInfo ? info.name || 'root' : info.name;
507
+ const desc = info.title ?? info.description ?? '';
508
+ lines.push(`- [${name}](${path})${desc ? ` — ${desc}` : ''}`);
509
+ }
510
+ lines.push('');
511
+ }
512
+
513
+ return `${lines.join('\n').trimEnd()}\n`;
514
+ }
515
+
516
+ // ============================================================================
517
+ // Main Entry Point
518
+ // ============================================================================
519
+
520
+ function resolveCommand(programOrCommand: object): AnyPadroneCommand {
521
+ if (commandSymbol in programOrCommand) return (programOrCommand as any)[commandSymbol] as AnyPadroneCommand;
522
+ return programOrCommand as AnyPadroneCommand;
523
+ }
524
+
525
+ /**
526
+ * Generate documentation for a Padrone CLI program or command tree.
527
+ * Accepts either a PadroneProgram (from createPadrone()) or a raw AnyPadroneCommand.
528
+ */
529
+ export function generateDocs(program: object, options: DocsOptions = {}): DocsResult {
530
+ const { format = 'markdown', output, includeHidden = false, frontmatter, overwrite = true, dryRun = false } = options;
531
+
532
+ const cmd = resolveCommand(program);
533
+ const allInfos = collectAllHelpInfo(cmd, includeHidden);
534
+ const rootInfo = allInfos[0]!;
535
+ const programName = cmd.name || 'program';
536
+
537
+ const pages: DocsPage[] = [];
538
+
539
+ const ext = format === 'markdown' ? '.md' : format === 'html' ? '.html' : format === 'man' ? '.1' : '.json';
540
+
541
+ for (let i = 0; i < allInfos.length; i++) {
542
+ const info = allInfos[i]!;
543
+ const isRoot = i === 0;
544
+ const depth = isRoot ? 0 : info.name.split(/\s+/).length;
545
+ const path = commandToPath(info, ext, isRoot);
546
+
547
+ let content: string;
548
+ switch (format) {
549
+ case 'markdown':
550
+ content = generateMarkdownPage(info, depth, frontmatter);
551
+ break;
552
+ case 'html':
553
+ content = generateHtmlPage(info, depth);
554
+ break;
555
+ case 'man':
556
+ content = generateManPage(info, depth, programName);
557
+ break;
558
+ case 'json':
559
+ content = `${JSON.stringify(info, null, 2)}\n`;
560
+ break;
561
+ }
562
+
563
+ pages.push({ path, content, command: info.name });
564
+ }
565
+
566
+ // Generate index page for markdown (when there are subcommands)
567
+ if (format === 'markdown' && allInfos.length > 1) {
568
+ // Replace the root page with a combined index
569
+ const rootPage = pages[0]!;
570
+ rootPage.content = generateMarkdownIndex(rootInfo, allInfos);
571
+ }
572
+
573
+ const result: DocsResult = { pages, written: [], skipped: [], errors: [] };
574
+
575
+ // Write to disk if output dir specified
576
+ if (output) {
577
+ const outDir = resolve(output);
578
+
579
+ for (const page of pages) {
580
+ const fullPath = join(outDir, page.path);
581
+
582
+ try {
583
+ if (existsSync(fullPath) && !overwrite) {
584
+ result.skipped.push(page.path);
585
+ continue;
586
+ }
587
+
588
+ if (dryRun) {
589
+ result.written.push(page.path);
590
+ continue;
591
+ }
592
+
593
+ const dir = dirname(fullPath);
594
+ mkdirSync(dir, { recursive: true });
595
+ writeFileSync(fullPath, page.content, 'utf-8');
596
+ result.written.push(page.path);
597
+ } catch (err) {
598
+ result.errors.push({
599
+ file: page.path,
600
+ error: err instanceof Error ? err : new Error(String(err)),
601
+ });
602
+ }
603
+ }
604
+ }
605
+
606
+ return result;
607
+ }