padrone 1.4.0 → 1.5.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 (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +105 -284
  3. package/dist/{args-CVDbyyzG.mjs → args-D5PNDyNu.mjs} +41 -18
  4. package/dist/args-D5PNDyNu.mjs.map +1 -0
  5. package/dist/chunk-CjcI7cDX.mjs +15 -0
  6. package/dist/codegen/index.d.mts +28 -3
  7. package/dist/codegen/index.d.mts.map +1 -1
  8. package/dist/codegen/index.mjs +169 -19
  9. package/dist/codegen/index.mjs.map +1 -1
  10. package/dist/command-utils-B1D-HqCd.mjs +1117 -0
  11. package/dist/command-utils-B1D-HqCd.mjs.map +1 -0
  12. package/dist/completion.d.mts +1 -1
  13. package/dist/completion.d.mts.map +1 -1
  14. package/dist/completion.mjs +77 -29
  15. package/dist/completion.mjs.map +1 -1
  16. package/dist/docs/index.d.mts +22 -2
  17. package/dist/docs/index.d.mts.map +1 -1
  18. package/dist/docs/index.mjs +94 -7
  19. package/dist/docs/index.mjs.map +1 -1
  20. package/dist/errors-BiVrBgi6.mjs +114 -0
  21. package/dist/errors-BiVrBgi6.mjs.map +1 -0
  22. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DtHzbP22.d.mts} +34 -5
  23. package/dist/formatter-DtHzbP22.d.mts.map +1 -0
  24. package/dist/help-bbmu9-qd.mjs +735 -0
  25. package/dist/help-bbmu9-qd.mjs.map +1 -0
  26. package/dist/index.d.mts +32 -3
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +493 -265
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/mcp-mLWIdUIu.mjs +379 -0
  31. package/dist/mcp-mLWIdUIu.mjs.map +1 -0
  32. package/dist/serve-B0u43DK7.mjs +404 -0
  33. package/dist/serve-B0u43DK7.mjs.map +1 -0
  34. package/dist/stream-BcC146Ud.mjs +56 -0
  35. package/dist/stream-BcC146Ud.mjs.map +1 -0
  36. package/dist/test.d.mts +1 -1
  37. package/dist/test.mjs +4 -15
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{types-DjIdJN5G.d.mts → types-Ch8Mk6Qb.d.mts} +310 -62
  40. package/dist/types-Ch8Mk6Qb.d.mts.map +1 -0
  41. package/dist/{update-check-EbNDkzyV.mjs → update-check-CFX1FV3v.mjs} +2 -2
  42. package/dist/{update-check-EbNDkzyV.mjs.map → update-check-CFX1FV3v.mjs.map} +1 -1
  43. package/dist/zod.d.mts +32 -0
  44. package/dist/zod.d.mts.map +1 -0
  45. package/dist/zod.mjs +50 -0
  46. package/dist/zod.mjs.map +1 -0
  47. package/package.json +10 -2
  48. package/src/args.ts +68 -40
  49. package/src/cli/docs.ts +1 -7
  50. package/src/cli/doctor.ts +195 -10
  51. package/src/cli/index.ts +1 -1
  52. package/src/cli/init.ts +2 -3
  53. package/src/cli/link.ts +2 -2
  54. package/src/codegen/discovery.ts +80 -28
  55. package/src/codegen/index.ts +2 -1
  56. package/src/codegen/parsers/bash.ts +179 -0
  57. package/src/codegen/schema-to-code.ts +2 -1
  58. package/src/colorizer.ts +126 -13
  59. package/src/command-utils.ts +380 -30
  60. package/src/completion.ts +120 -47
  61. package/src/create.ts +480 -128
  62. package/src/docs/index.ts +122 -8
  63. package/src/formatter.ts +171 -125
  64. package/src/help.ts +45 -12
  65. package/src/index.ts +29 -1
  66. package/src/interactive.ts +45 -4
  67. package/src/mcp.ts +390 -0
  68. package/src/repl-loop.ts +16 -3
  69. package/src/runtime.ts +195 -2
  70. package/src/serve.ts +442 -0
  71. package/src/stream.ts +75 -0
  72. package/src/test.ts +7 -16
  73. package/src/type-utils.ts +28 -4
  74. package/src/types.ts +212 -30
  75. package/src/wrap.ts +23 -25
  76. package/src/zod.ts +50 -0
  77. package/dist/args-CVDbyyzG.mjs.map +0 -1
  78. package/dist/chunk-y_GBKt04.mjs +0 -5
  79. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  80. package/dist/help-CcBe91bV.mjs +0 -1254
  81. package/dist/help-CcBe91bV.mjs.map +0 -1
  82. package/dist/types-DjIdJN5G.d.mts.map +0 -1
package/src/formatter.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { camelToKebab } from './args.ts';
2
- import { createColorizer } from './colorizer.ts';
2
+ import { type ColorConfig, type ColorTheme, createColorizer } from './colorizer.ts';
3
3
 
4
4
  export type HelpFormat = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json';
5
5
  export type HelpDetail = 'minimal' | 'standard' | 'full';
@@ -45,6 +45,8 @@ export type HelpArgumentInfo = {
45
45
  negatable?: boolean;
46
46
  /** Config file key that maps to this arg */
47
47
  configKey?: string;
48
+ /** Group name for organizing this option under a labeled section in help output */
49
+ group?: string;
48
50
  };
49
51
 
50
52
  /**
@@ -58,6 +60,8 @@ export type HelpSubcommandInfo = {
58
60
  deprecated?: boolean | string;
59
61
  hidden?: boolean;
60
62
  hasSubcommands?: boolean;
63
+ /** Group name for organizing this command under a labeled section in help output */
64
+ group?: string;
61
65
  };
62
66
 
63
67
  /**
@@ -103,6 +107,8 @@ export type HelpInfo = {
103
107
  arguments?: HelpArgumentInfo[];
104
108
  /** Built-in commands and flags (shown only for root command) */
105
109
  builtins?: HelpBuiltinInfo[];
110
+ /** Command-level usage examples (shown in help output) */
111
+ examples?: string[];
106
112
  /** Full help info for nested commands (used in 'full' detail mode) */
107
113
  nestedCommands?: HelpInfo[];
108
114
  };
@@ -133,6 +139,7 @@ type Styler = {
133
139
  type: (text: string) => string;
134
140
  description: (text: string) => string;
135
141
  label: (text: string) => string;
142
+ section: (text: string) => string;
136
143
  meta: (text: string) => string;
137
144
  example: (text: string) => string;
138
145
  exampleValue: (text: string) => string;
@@ -147,7 +154,6 @@ type LayoutConfig = {
147
154
  indent: (level: number) => string;
148
155
  join: (parts: string[]) => string;
149
156
  wrapDocument?: (content: string) => string;
150
- usageLabel: string;
151
157
  };
152
158
 
153
159
  // ============================================================================
@@ -161,6 +167,7 @@ function createTextStyler(): Styler {
161
167
  type: (text) => text,
162
168
  description: (text) => text,
163
169
  label: (text) => text,
170
+ section: (text) => text,
164
171
  meta: (text) => text,
165
172
  example: (text) => text,
166
173
  exampleValue: (text) => text,
@@ -168,14 +175,15 @@ function createTextStyler(): Styler {
168
175
  };
169
176
  }
170
177
 
171
- function createAnsiStyler(): Styler {
172
- const colorizer = createColorizer();
178
+ function createAnsiStyler(theme?: ColorTheme | ColorConfig): Styler {
179
+ const colorizer = createColorizer(theme);
173
180
  return {
174
181
  command: colorizer.command,
175
182
  arg: colorizer.arg,
176
183
  type: colorizer.type,
177
184
  description: colorizer.description,
178
185
  label: colorizer.label,
186
+ section: colorizer.label,
179
187
  meta: colorizer.meta,
180
188
  example: colorizer.example,
181
189
  exampleValue: colorizer.exampleValue,
@@ -183,30 +191,8 @@ function createAnsiStyler(): Styler {
183
191
  };
184
192
  }
185
193
 
186
- function createConsoleStyler(): Styler {
187
- const colors = {
188
- reset: '\x1b[0m',
189
- bold: '\x1b[1m',
190
- dim: '\x1b[2m',
191
- italic: '\x1b[3m',
192
- underline: '\x1b[4m',
193
- strikethrough: '\x1b[9m',
194
- cyan: '\x1b[36m',
195
- green: '\x1b[32m',
196
- yellow: '\x1b[33m',
197
- gray: '\x1b[90m',
198
- };
199
- return {
200
- command: (text) => `${colors.cyan}${colors.bold}${text}${colors.reset}`,
201
- arg: (text) => `${colors.green}${text}${colors.reset}`,
202
- type: (text) => `${colors.yellow}${text}${colors.reset}`,
203
- description: (text) => `${colors.dim}${text}${colors.reset}`,
204
- label: (text) => `${colors.bold}${text}${colors.reset}`,
205
- meta: (text) => `${colors.gray}${text}${colors.reset}`,
206
- example: (text) => `${colors.underline}${text}${colors.reset}`,
207
- exampleValue: (text) => `${colors.italic}${text}${colors.reset}`,
208
- deprecated: (text) => `${colors.strikethrough}${colors.gray}${text}${colors.reset}`,
209
- };
194
+ function createConsoleStyler(theme?: ColorTheme | ColorConfig): Styler {
195
+ return createAnsiStyler(theme);
210
196
  }
211
197
 
212
198
  function createMarkdownStyler(): Styler {
@@ -215,7 +201,8 @@ function createMarkdownStyler(): Styler {
215
201
  arg: (text) => `\`${text}\``,
216
202
  type: (text) => `\`${text}\``,
217
203
  description: (text) => text,
218
- label: (text) => `### ${text}`,
204
+ label: (text) => `**${text}**`,
205
+ section: (text) => `### ${text}`,
219
206
  meta: (text) => `*${text}*`,
220
207
  example: (text) => `**${text}**`,
221
208
  exampleValue: (text) => `\`${text}\``,
@@ -233,7 +220,8 @@ function createHtmlStyler(): Styler {
233
220
  arg: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
234
221
  type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
235
222
  description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
236
- label: (text) => `<h3>${escapeHtml(text)}</h3>`,
223
+ label: (text) => `<strong>${escapeHtml(text)}</strong>`,
224
+ section: (text) => `<h3>${escapeHtml(text)}</h3>`,
237
225
  meta: (text) => `<span style="color: #999;">${escapeHtml(text)}</span>`,
238
226
  example: (text) => `<strong style="text-decoration: underline;">${escapeHtml(text)}</strong>`,
239
227
  exampleValue: (text) => `<em>${escapeHtml(text)}</em>`,
@@ -250,7 +238,6 @@ function createTextLayout(): LayoutConfig {
250
238
  newline: '\n',
251
239
  indent: (level) => ' '.repeat(level),
252
240
  join: (parts) => parts.filter(Boolean).join(' '),
253
- usageLabel: 'Usage:',
254
241
  };
255
242
  }
256
243
 
@@ -263,7 +250,6 @@ function createMarkdownLayout(): LayoutConfig {
263
250
  return ' ';
264
251
  },
265
252
  join: (parts) => parts.filter(Boolean).join(' '),
266
- usageLabel: 'Usage:',
267
253
  };
268
254
  }
269
255
 
@@ -273,7 +259,6 @@ function createHtmlLayout(): LayoutConfig {
273
259
  indent: (level) => '&nbsp;&nbsp;'.repeat(level),
274
260
  join: (parts) => parts.filter(Boolean).join(' '),
275
261
  wrapDocument: (content) => `<div style="font-family: monospace; line-height: 1.6;">${content}</div>`,
276
- usageLabel: '<strong>Usage:</strong>',
277
262
  };
278
263
  }
279
264
 
@@ -284,8 +269,8 @@ function createHtmlLayout(): LayoutConfig {
284
269
  /**
285
270
  * Creates a formatter that uses the given styler and layout configuration.
286
271
  */
287
- function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter {
288
- const { newline, indent, join, wrapDocument, usageLabel } = layout;
272
+ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBuiltins?: boolean): Formatter {
273
+ const { newline, indent, join, wrapDocument } = layout;
289
274
 
290
275
  function formatUsageSection(info: HelpInfo): string[] {
291
276
  const usageParts: string[] = [styler.command(info.usage.command), info.usage.hasSubcommands ? styler.meta('[command]') : ''];
@@ -297,15 +282,13 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
297
282
  }
298
283
  }
299
284
  if (info.usage.hasArguments) usageParts.push(styler.meta('[options]'));
300
- return [`${usageLabel} ${join(usageParts)}`];
285
+ return [`${styler.label('Usage:')} ${join(usageParts)}`];
301
286
  }
302
287
 
303
288
  function formatSubcommandsSection(info: HelpInfo): string[] {
304
289
  const lines: string[] = [];
305
290
  const subcommands = info.subcommands!;
306
291
 
307
- lines.push(styler.label('Commands:'));
308
-
309
292
  const subcommandSuffix = (c: HelpSubcommandInfo) => (c.hasSubcommands ? ' <subcommand>' : '');
310
293
  const formatAliasParts = (c: HelpSubcommandInfo) => {
311
294
  if (!c.aliases?.length) return { plain: '', styled: '' };
@@ -323,40 +306,55 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
323
306
  }
324
307
  return { plain: parts.length ? ` ${parts.join(' ')}` : '', styled: styledParts.length ? ` ${styledParts.join(' ')}` : '' };
325
308
  };
309
+ // Column width is computed across all subcommands for consistent alignment
326
310
  const maxNameLength = Math.max(
327
311
  ...subcommands.map((c) => {
328
312
  return (c.name + subcommandSuffix(c) + formatAliasParts(c).plain).length;
329
313
  }),
330
314
  );
331
- for (const subCmd of subcommands) {
332
- const aliasParts = formatAliasParts(subCmd);
333
- const suffix = subcommandSuffix(subCmd);
334
- const commandDisplay = subCmd.name + suffix + aliasParts.plain;
335
- const padding = ' '.repeat(Math.max(0, maxNameLength - commandDisplay.length + 2));
336
- const isDeprecated = !!subCmd.deprecated;
337
- const isDefaultEntry = subCmd.name === '[default]';
338
- const commandName = isDeprecated
339
- ? styler.deprecated(commandDisplay)
340
- : (isDefaultEntry ? styler.meta(subCmd.name) : styler.command(subCmd.name)) +
341
- (suffix ? styler.meta(suffix) : '') +
342
- aliasParts.styled;
343
- const lineParts: string[] = [commandName, padding];
344
-
345
- // Use title if available, otherwise use description
346
- const displayText = subCmd.title ?? subCmd.description;
347
- if (displayText) {
348
- lineParts.push(isDeprecated ? styler.deprecated(displayText) : styler.description(displayText));
349
- }
350
- if (isDeprecated) {
351
- const deprecatedMeta =
352
- typeof subCmd.deprecated === 'string' ? styler.meta(` (deprecated: ${subCmd.deprecated})`) : styler.meta(' (deprecated)');
353
- lineParts.push(deprecatedMeta);
315
+
316
+ const grouped = Object.groupBy(subcommands, (c) => c.group ?? '');
317
+
318
+ const renderSubcommands = (cmds: HelpSubcommandInfo[]) => {
319
+ for (const subCmd of cmds) {
320
+ const aliasParts = formatAliasParts(subCmd);
321
+ const suffix = subcommandSuffix(subCmd);
322
+ const commandDisplay = subCmd.name + suffix + aliasParts.plain;
323
+ const padding = ' '.repeat(Math.max(0, maxNameLength - commandDisplay.length + 2));
324
+ const isDeprecated = !!subCmd.deprecated;
325
+ const isDefaultEntry = subCmd.name === '[default]';
326
+ const commandName = isDeprecated
327
+ ? styler.deprecated(commandDisplay)
328
+ : (isDefaultEntry ? styler.meta(subCmd.name) : styler.command(subCmd.name)) +
329
+ (suffix ? styler.meta(suffix) : '') +
330
+ aliasParts.styled;
331
+ const lineParts: string[] = [commandName, padding];
332
+
333
+ // Use title if available, otherwise use description
334
+ const displayText = subCmd.title ?? subCmd.description;
335
+ if (displayText) {
336
+ lineParts.push(isDeprecated ? styler.deprecated(displayText) : styler.description(displayText));
337
+ }
338
+ if (isDeprecated) {
339
+ const deprecatedMeta =
340
+ typeof subCmd.deprecated === 'string' ? styler.meta(` (deprecated: ${subCmd.deprecated})`) : styler.meta(' (deprecated)');
341
+ lineParts.push(deprecatedMeta);
342
+ }
343
+ lines.push(indent(1) + lineParts.join(''));
354
344
  }
355
- lines.push(indent(1) + lineParts.join(''));
345
+ };
346
+
347
+ for (const [key, items] of Object.entries(grouped)) {
348
+ if (lines.length > 0) lines.push('');
349
+ lines.push(styler.section(key ? `${key}:` : 'Commands:'));
350
+ renderSubcommands(items!);
356
351
  }
357
352
 
358
- lines.push('');
359
- lines.push(styler.meta(`Run "${info.name} [command] --help" for more information on a command.`));
353
+ // Skip hint when builtins are present — the builtins section shows a combined hint
354
+ if (!info.builtins?.length) {
355
+ lines.push('');
356
+ lines.push(styler.meta(`Run "${info.name} [command] --help" for more information on a command.`));
357
+ }
360
358
 
361
359
  return lines;
362
360
  }
@@ -365,7 +363,7 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
365
363
  const lines: string[] = [];
366
364
  const args = info.positionals!;
367
365
 
368
- lines.push(styler.label('Arguments:'));
366
+ lines.push(styler.section('Arguments:'));
369
367
 
370
368
  const maxNameLength = Math.min(32, Math.max(...args.map((a) => a.name.length)));
371
369
 
@@ -386,8 +384,6 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
386
384
  const lines: string[] = [];
387
385
  const argList = info.arguments || [];
388
386
 
389
- lines.push(styler.label('Options:'));
390
-
391
387
  // Helper to check if a default value is meaningful (not empty string/array)
392
388
  const hasDefault = (value: unknown): boolean => {
393
389
  if (value === undefined) return false;
@@ -396,70 +392,94 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
396
392
  return true;
397
393
  };
398
394
 
399
- // Build left column (signature) for each arg to compute alignment
400
- const argColumns: { plain: string; styled: string; arg: HelpArgumentInfo }[] = argList.map((arg) => {
395
+ // Build columns: flags | names | type | description
396
+ const argColumns = argList.map((arg) => {
401
397
  // Promote kebab-case alias to primary display name if it exists
402
398
  const kebab = camelToKebab(arg.name);
403
399
  const primaryName = kebab && arg.aliases?.includes(kebab) ? kebab : arg.name;
404
400
  const remainingAliases = arg.aliases?.filter((a) => a !== primaryName);
405
- const argName = `--${primaryName}`;
406
- const flagNames = arg.flags?.length ? arg.flags.map((f) => `-${f}`).join(', ') : '';
407
- const aliasNames = remainingAliases?.length ? remainingAliases.map((a) => `--${a}`).join(', ') : '';
408
- const shortNames = [flagNames, aliasNames].filter(Boolean).join(', ');
409
- const fullArgName = shortNames ? `${argName}, ${shortNames}` : argName;
410
- const isDeprecated = !!arg.deprecated;
411
- const formattedArgName = isDeprecated ? styler.deprecated(fullArgName) : styler.arg(fullArgName);
412
401
 
413
- const plainParts: string[] = [fullArgName];
414
- const styledParts: string[] = [formattedArgName];
402
+ const flagsPlain = arg.flags?.length ? arg.flags.map((f) => `-${f}`).join(', ') : '';
403
+ const namesPlain = [`--${primaryName}`, ...(remainingAliases?.map((a) => `--${a}`) || [])].join(', ');
404
+ const typePlain = arg.type && arg.type !== 'boolean' ? (arg.optional ? `[${arg.type}]` : `<${arg.type}>`) : '';
415
405
 
416
- if (arg.type && arg.type !== 'boolean') {
417
- const typePart = arg.optional ? `[${arg.type}]` : `<${arg.type}>`;
418
- plainParts.push(typePart);
419
- styledParts.push(styler.type(typePart));
420
- }
421
- if (isDeprecated) {
422
- const deprecatedPart = typeof arg.deprecated === 'string' ? `(deprecated: ${arg.deprecated})` : '(deprecated)';
423
- plainParts.push(deprecatedPart);
424
- styledParts.push(styler.meta(deprecatedPart));
425
- }
406
+ const isDeprecated = !!arg.deprecated;
426
407
 
427
- return { plain: plainParts.join(' '), styled: join(styledParts), arg };
408
+ return { flagsPlain, namesPlain, typePlain, isDeprecated, arg };
428
409
  });
429
410
 
430
- const maxColumnWidth = Math.min(32, Math.max(...argColumns.map((c) => c.plain.length)));
431
-
432
- for (const { plain, styled, arg } of argColumns) {
433
- const padding = ' '.repeat(Math.max(2, maxColumnWidth - plain.length + 2));
434
-
435
- // Description followed by metadata (choices, default)
436
- const descParts: string[] = [];
437
- if (arg.description) descParts.push(styler.description(arg.description));
438
- if (info.usage.stdinField === arg.name) descParts.push(styler.meta('(stdin)'));
439
- if (arg.enum) descParts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
440
- if (hasDefault(arg.default)) descParts.push(styler.meta(`(default: ${String(arg.default)})`));
411
+ // Column widths are computed across all args (regardless of group) for consistent alignment
412
+ const maxFlagsWidth = Math.min(12, Math.max(0, ...argColumns.map((c) => c.flagsPlain.length)));
413
+ const maxNamesWidth = Math.min(32, Math.max(0, ...argColumns.map((c) => c.namesPlain.length)));
414
+ const maxTypeWidth = Math.min(16, Math.max(0, ...argColumns.map((c) => c.typePlain.length)));
415
+ const hasAnyFlags = maxFlagsWidth > 0;
416
+
417
+ // Split into ordered groups: ungrouped first as "Options:", then each group in first-seen order
418
+ const grouped = Object.groupBy(argColumns, (c) => c.arg.group ?? '');
419
+
420
+ const renderArgColumns = (columns: typeof argColumns) => {
421
+ for (const { flagsPlain, namesPlain, typePlain, isDeprecated, arg } of columns) {
422
+ const parts: string[] = [];
423
+
424
+ // Flags column
425
+ if (hasAnyFlags) {
426
+ const styledFlags = isDeprecated ? (flagsPlain ? styler.deprecated(flagsPlain) : '') : flagsPlain ? styler.arg(flagsPlain) : '';
427
+ const flagsPadding = ' '.repeat(Math.max(0, maxFlagsWidth - flagsPlain.length));
428
+ const separator = flagsPlain ? ', ' : ' ';
429
+ parts.push(styledFlags + flagsPadding + separator);
430
+ }
441
431
 
442
- lines.push(indent(1) + styled + padding + join(descParts));
432
+ // Names column
433
+ const styledNames = isDeprecated ? styler.deprecated(namesPlain) : styler.arg(namesPlain);
434
+ const namesPadding = ' '.repeat(Math.max(2, maxNamesWidth - namesPlain.length + 2));
435
+ parts.push(styledNames + namesPadding);
443
436
 
444
- // Environment variable line
445
- if (arg.env) {
446
- const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
447
- const envParts: string[] = [styler.example('Env:'), styler.exampleValue(envVars.join(', '))];
448
- lines.push(indent(3) + join(envParts));
449
- }
437
+ // Type column
438
+ if (maxTypeWidth > 0) {
439
+ const styledType = typePlain ? styler.type(typePlain) : '';
440
+ const typePadding = ' '.repeat(Math.max(2, maxTypeWidth - typePlain.length + 2));
441
+ parts.push(styledType + typePadding);
442
+ }
450
443
 
451
- // Config key line
452
- if (arg.configKey) {
453
- const configParts: string[] = [styler.example('Config:'), styler.exampleValue(arg.configKey)];
454
- lines.push(indent(3) + join(configParts));
444
+ // Line 1: description
445
+ const descParts: string[] = [];
446
+ if (arg.description) descParts.push(isDeprecated ? styler.deprecated(arg.description) : styler.description(arg.description));
447
+ lines.push(indent(1) + parts.join('') + join(descParts));
448
+
449
+ // Line 2: deprecated (no reason), default, choices
450
+ const line2Parts: string[] = [];
451
+ if (isDeprecated && typeof arg.deprecated !== 'string') line2Parts.push(styler.meta('(deprecated)'));
452
+ if (hasDefault(arg.default)) line2Parts.push(styler.meta(`(default: ${String(arg.default)})`));
453
+ if (arg.enum) line2Parts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
454
+ if (line2Parts.length > 0) lines.push(indent(3) + join(line2Parts));
455
+
456
+ // Line 3: deprecated (with reason), examples
457
+ const line3Parts: string[] = [];
458
+ if (isDeprecated && typeof arg.deprecated === 'string') line3Parts.push(styler.meta(`(deprecated: ${arg.deprecated})`));
459
+ if (arg.examples && arg.examples.length > 0) {
460
+ const exampleValues = arg.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
461
+ line3Parts.push(styler.example('Example:'), styler.exampleValue(exampleValues));
462
+ }
463
+ if (line3Parts.length > 0) lines.push(indent(3) + join(line3Parts));
464
+
465
+ // Line 4: stdin, env, config
466
+ const line4Parts: string[] = [];
467
+ if (info.usage.stdinField === arg.name) line4Parts.push(styler.meta('(stdin)'));
468
+ if (arg.env) {
469
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
470
+ line4Parts.push(styler.example('Env:'), styler.exampleValue(envVars.join(', ')));
471
+ }
472
+ if (arg.configKey) {
473
+ line4Parts.push(styler.example('Config:'), styler.exampleValue(arg.configKey));
474
+ }
475
+ if (line4Parts.length > 0) lines.push(indent(3) + join(line4Parts));
455
476
  }
477
+ };
456
478
 
457
- // Examples line
458
- if (arg.examples && arg.examples.length > 0) {
459
- const exampleValues = arg.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
460
- const exampleParts: string[] = [styler.example('Example:'), styler.exampleValue(exampleValues)];
461
- lines.push(indent(3) + join(exampleParts));
462
- }
479
+ for (const [key, items] of Object.entries(grouped)) {
480
+ if (lines.length > 0) lines.push('');
481
+ lines.push(styler.section(key ? `${key}:` : 'Options:'));
482
+ renderArgColumns(items!);
463
483
  }
464
484
 
465
485
  return lines;
@@ -469,7 +489,18 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
469
489
  const lines: string[] = [];
470
490
  const builtins = info.builtins!;
471
491
 
472
- lines.push(styler.label('Built-in:'));
492
+ if (!showAllBuiltins) {
493
+ // Collapsed summary: show selected builtins on a single line with a combined hint
494
+ const highlights = ['help [command]', 'version', '[command] --repl'];
495
+ lines.push(`${styler.label('Global:')} ${styler.meta(highlights.join(', '))}`);
496
+ const hint = info.usage.hasSubcommands
497
+ ? `Run "${info.name} [command] --help" for more information. Use "--all" for all global commands.`
498
+ : `Use "${info.name} help --all" for more information on global commands.`;
499
+ lines.push(styler.meta(hint));
500
+ return lines;
501
+ }
502
+
503
+ lines.push(styler.section('Global:'));
473
504
 
474
505
  // Compute max effective name length for alignment across main and sub entries
475
506
  const allLengths: number[] = [];
@@ -537,6 +568,15 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
537
568
  lines.push('');
538
569
  }
539
570
 
571
+ // Examples section (if present)
572
+ if (info.examples && info.examples.length > 0) {
573
+ lines.push(styler.section('Examples:'));
574
+ for (const ex of info.examples) {
575
+ lines.push(indent(1) + styler.meta('$ ') + styler.exampleValue(ex));
576
+ }
577
+ lines.push('');
578
+ }
579
+
540
580
  // Subcommands section
541
581
  if (info.subcommands && info.subcommands.length > 0) {
542
582
  lines.push(...formatSubcommandsSection(info));
@@ -566,7 +606,7 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
566
606
 
567
607
  // Nested commands section (full detail mode)
568
608
  if (info.nestedCommands?.length) {
569
- lines.push(styler.label('Subcommand Details:'));
609
+ lines.push(styler.section('Subcommand Details:'));
570
610
  lines.push('');
571
611
  for (const nestedCmd of info.nestedCommands) {
572
612
  lines.push(styler.meta('─'.repeat(60)));
@@ -628,12 +668,18 @@ function createMinimalFormatter(): Formatter {
628
668
  };
629
669
  }
630
670
 
631
- export function createFormatter(format: HelpFormat | 'auto', detail: HelpDetail = 'standard'): Formatter {
671
+ export function createFormatter(
672
+ format: HelpFormat | 'auto',
673
+ detail: HelpDetail = 'standard',
674
+ theme?: ColorTheme | ColorConfig,
675
+ all?: boolean,
676
+ ): Formatter {
632
677
  if (detail === 'minimal') return createMinimalFormatter();
633
678
  if (format === 'json') return createJsonFormatter();
634
- if (format === 'ansi' || (format === 'auto' && shouldUseAnsi())) return createGenericFormatter(createAnsiStyler(), createTextLayout());
635
- if (format === 'console') return createGenericFormatter(createConsoleStyler(), createTextLayout());
636
- if (format === 'markdown') return createGenericFormatter(createMarkdownStyler(), createMarkdownLayout());
637
- if (format === 'html') return createGenericFormatter(createHtmlStyler(), createHtmlLayout());
638
- return createGenericFormatter(createTextStyler(), createTextLayout());
679
+ if (format === 'ansi' || (format === 'auto' && shouldUseAnsi()))
680
+ return createGenericFormatter(createAnsiStyler(theme), createTextLayout(), all);
681
+ if (format === 'console') return createGenericFormatter(createConsoleStyler(theme), createTextLayout(), all);
682
+ if (format === 'markdown') return createGenericFormatter(createMarkdownStyler(), createMarkdownLayout(), all);
683
+ if (format === 'html') return createGenericFormatter(createHtmlStyler(), createHtmlLayout(), all);
684
+ return createGenericFormatter(createTextStyler(), createTextLayout(), all);
639
685
  }
package/src/help.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
2
- import { extractSchemaMetadata, type PadroneArgsSchemaMeta, parsePositionalConfig, parseStdinConfig } from './args.ts';
2
+ import { extractSchemaMetadata, JSON_SCHEMA_OPTS, type PadroneArgsSchemaMeta, parsePositionalConfig, parseStdinConfig } from './args.ts';
3
+ import type { ColorConfig, ColorTheme } from './colorizer.ts';
3
4
  import { findCommandByName } from './command-utils.ts';
4
5
  import {
5
6
  createFormatter,
@@ -16,6 +17,9 @@ import { getRootCommand } from './utils.ts';
16
17
  export type HelpPreferences = {
17
18
  format?: HelpFormat | 'auto';
18
19
  detail?: HelpDetail;
20
+ theme?: ColorTheme | ColorConfig;
21
+ /** Show all global commands and flags in full detail */
22
+ all?: boolean;
19
23
  };
20
24
 
21
25
  /**
@@ -35,7 +39,7 @@ function extractPositionalArgsInfo(
35
39
  const positionalConfig = parsePositionalConfig(meta.positional);
36
40
 
37
41
  try {
38
- const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
42
+ const jsonSchema = schema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
39
43
 
40
44
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
41
45
  const properties = jsonSchema.properties as Record<string, any>;
@@ -75,7 +79,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
75
79
  const argsMeta = meta?.fields;
76
80
 
77
81
  try {
78
- const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
82
+ const jsonSchema = schema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
79
83
 
80
84
  // Handle object: z.object({ key: z.string(), ... })
81
85
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
@@ -134,6 +138,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
134
138
  examples: optMeta?.examples ?? prop?.examples,
135
139
  variadic: propType === 'array',
136
140
  negatable: isNegatable,
141
+ group: optMeta?.group,
137
142
  });
138
143
  }
139
144
  }
@@ -154,7 +159,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
154
159
  * @param cmd - The command to build help info for
155
160
  * @param detail - The level of detail ('minimal', 'standard', or 'full')
156
161
  */
157
- export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard'): HelpInfo {
162
+ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard', all?: boolean): HelpInfo {
158
163
  const rootCmd = getRootCommand(cmd);
159
164
  // A command is a "default" command if its name is '' or it has '' as an alias
160
165
  const isDefaultCommand = cmd.parent && (!cmd.name || cmd.aliases?.includes(''));
@@ -176,6 +181,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
176
181
  name: commandName,
177
182
  title: cmd.title,
178
183
  description: cmd.description,
184
+ examples: cmd.examples,
179
185
  aliases: displayAliases,
180
186
  deprecated: cmd.deprecated,
181
187
  hidden: cmd.hidden,
@@ -184,7 +190,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
184
190
  hasSubcommands: !!(cmd.commands && cmd.commands.length > 0),
185
191
  hasPositionals,
186
192
  hasArguments: false, // updated below after extracting arguments
187
- stdinField: cmd.meta?.stdin ? parseStdinConfig(cmd.meta.stdin).field : undefined,
193
+ stdinField: cmd.meta?.stdin ? parseStdinConfig(cmd.meta.stdin) : undefined,
188
194
  },
189
195
  };
190
196
 
@@ -222,6 +228,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
222
228
  aliases: displayAliases?.length ? displayAliases : undefined,
223
229
  deprecated: c.deprecated,
224
230
  hidden: c.hidden,
231
+ group: c.group,
225
232
  },
226
233
  {
227
234
  name: displayName,
@@ -230,6 +237,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
230
237
  deprecated: c.deprecated,
231
238
  hidden: c.hidden,
232
239
  hasSubcommands: true,
240
+ group: c.group,
233
241
  },
234
242
  ];
235
243
  }
@@ -243,6 +251,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
243
251
  deprecated: c.deprecated,
244
252
  hidden: c.hidden,
245
253
  hasSubcommands,
254
+ group: c.group,
246
255
  },
247
256
  ];
248
257
  }),
@@ -285,40 +294,64 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
285
294
  }
286
295
  }
287
296
 
288
- // Add built-in commands/flags for root command only
289
- if (!cmd.parent) {
297
+ // Add global commands/flags (root command by default, all commands when --all is passed)
298
+ if (!cmd.parent || all) {
290
299
  const builtins: HelpInfo['builtins'] = [];
291
300
 
292
- if (!findCommandByName('help', cmd.commands)) {
301
+ if (!findCommandByName('help', rootCmd.commands)) {
293
302
  builtins.push({
294
303
  name: 'help [command], -h, --help',
295
304
  description: 'Show help for a command',
296
305
  sub: [
306
+ { name: '--all', description: 'Show all global commands and flags' },
297
307
  { name: '--detail <level>', description: 'Detail level (minimal, standard, full)' },
298
308
  { name: '--format <format>', description: 'Output format (text, ansi, json, markdown, html)' },
299
309
  ],
300
310
  });
301
311
  }
302
312
 
303
- if (!findCommandByName('version', cmd.commands)) {
313
+ if (!findCommandByName('version', rootCmd.commands)) {
304
314
  builtins.push({
305
315
  name: 'version, -v, --version',
306
316
  description: 'Show version information',
307
317
  });
308
318
  }
309
319
 
310
- if (!findCommandByName('completion', cmd.commands)) {
320
+ if (!findCommandByName('completion', rootCmd.commands)) {
311
321
  builtins.push({
312
322
  name: 'completion [shell]',
313
323
  description: 'Generate shell completions (bash, zsh, fish, powershell)',
314
324
  });
315
325
  }
316
326
 
327
+ if (!findCommandByName('man', rootCmd.commands)) {
328
+ builtins.push({
329
+ name: 'man',
330
+ description: 'Show or install man pages (--setup to install, --remove to uninstall) (experimental)',
331
+ });
332
+ }
333
+
317
334
  builtins.push({
318
335
  name: '[command] --repl',
319
336
  description: 'Start interactive REPL scoped to a command',
320
337
  });
321
338
 
339
+ if (!findCommandByName('mcp', rootCmd.commands)) {
340
+ builtins.push({
341
+ name: 'mcp [http|stdio]',
342
+ description: 'Start a Model Context Protocol server to expose commands as AI tools (experimental)',
343
+ sub: [
344
+ { name: '--port <port>', description: 'HTTP port (default: 3000)' },
345
+ { name: '--host <host>', description: 'HTTP host (default: 127.0.0.1)' },
346
+ ],
347
+ });
348
+ }
349
+
350
+ builtins.push({
351
+ name: '--color [theme], --no-color',
352
+ description: 'Set color theme (default, ocean, warm, monochrome) or disable colors',
353
+ });
354
+
322
355
  if (builtins.length > 0) {
323
356
  helpInfo.builtins = builtins;
324
357
  }
@@ -332,7 +365,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
332
365
  // ============================================================================
333
366
 
334
367
  export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, prefs?: HelpPreferences): string {
335
- const helpInfo = getHelpInfo(commandObj, prefs?.detail);
336
- const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail);
368
+ const helpInfo = getHelpInfo(commandObj, prefs?.detail, prefs?.all);
369
+ const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail, prefs?.theme, prefs?.all);
337
370
  return formatter.format(helpInfo);
338
371
  }