padrone 1.1.0 → 1.3.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 +97 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  4. package/dist/args-DFEI7_G_.mjs +197 -0
  5. package/dist/args-DFEI7_G_.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 +1358 -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 +405 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-XroimS3Q.d.mts +83 -0
  20. package/dist/formatter-XroimS3Q.d.mts.map +1 -0
  21. package/dist/help-CgGP7hQU.mjs +1229 -0
  22. package/dist/help-CgGP7hQU.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 +1220 -1204
  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-BS7RP5Ls.d.mts +1059 -0
  32. package/dist/types-BS7RP5Ls.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 +457 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +330 -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 +197 -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 +504 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1048 -308
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +195 -73
  66. package/src/help.ts +159 -58
  67. package/src/index.ts +12 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +52 -21
  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
package/src/errors.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Structured error hierarchy for Padrone CLI framework.
3
+ *
4
+ * All Padrone errors extend `PadroneError`, which carries an exit code,
5
+ * optional suggestions, and context about which command/phase produced the error.
6
+ * This allows callers to distinguish user errors (bad input) from bugs (unexpected throws)
7
+ * and to present formatted, actionable error messages.
8
+ */
9
+
10
+ export type PadroneErrorOptions = {
11
+ /** Process exit code. Defaults to 1. */
12
+ exitCode?: number;
13
+ /** Actionable suggestions shown to the user (e.g. "Use --env production"). */
14
+ suggestions?: string[];
15
+ /** The command path that produced the error (e.g. "deploy staging"). */
16
+ command?: string;
17
+ /** The phase where the error occurred. */
18
+ phase?: 'parse' | 'validate' | 'execute' | 'config';
19
+ /** Original cause for error chaining. */
20
+ cause?: unknown;
21
+ };
22
+
23
+ /**
24
+ * Base error class for all Padrone errors.
25
+ * Carries structured metadata for user-friendly formatting and programmatic handling.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * throw new PadroneError('Something went wrong', {
30
+ * exitCode: 1,
31
+ * suggestions: ['Try --help for usage information'],
32
+ * });
33
+ * ```
34
+ */
35
+ export class PadroneError extends Error {
36
+ readonly exitCode: number;
37
+ readonly suggestions: string[];
38
+ readonly command?: string;
39
+ readonly phase?: 'parse' | 'validate' | 'execute' | 'config';
40
+
41
+ constructor(message: string, options?: PadroneErrorOptions) {
42
+ super(message, options?.cause ? { cause: options.cause } : undefined);
43
+ this.name = 'PadroneError';
44
+ this.exitCode = options?.exitCode ?? 1;
45
+ this.suggestions = options?.suggestions ?? [];
46
+ this.command = options?.command;
47
+ this.phase = options?.phase;
48
+ }
49
+
50
+ /**
51
+ * Returns a serializable representation of the error,
52
+ * suitable for non-terminal runtimes (web UIs, APIs, etc.).
53
+ */
54
+ toJSON(): {
55
+ name: string;
56
+ message: string;
57
+ exitCode: number;
58
+ suggestions: string[];
59
+ command?: string;
60
+ phase?: string;
61
+ } {
62
+ return {
63
+ name: this.name,
64
+ message: this.message,
65
+ exitCode: this.exitCode,
66
+ suggestions: this.suggestions,
67
+ command: this.command,
68
+ phase: this.phase,
69
+ };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Thrown when command routing fails — unknown command, unexpected arguments, etc.
75
+ */
76
+ export class RoutingError extends PadroneError {
77
+ constructor(message: string, options?: PadroneErrorOptions) {
78
+ super(message, { phase: 'parse', ...options });
79
+ this.name = 'RoutingError';
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Thrown when argument or schema validation fails.
85
+ * Carries the structured issues from the schema validator.
86
+ */
87
+ export class ValidationError extends PadroneError {
88
+ readonly issues: readonly { path?: PropertyKey[]; message: string }[];
89
+
90
+ constructor(message: string, issues: readonly { path?: PropertyKey[]; message: string }[], options?: PadroneErrorOptions) {
91
+ super(message, { phase: 'validate', ...options });
92
+ this.name = 'ValidationError';
93
+ this.issues = issues;
94
+ }
95
+
96
+ override toJSON() {
97
+ return {
98
+ ...super.toJSON(),
99
+ issues: this.issues.map((i) => ({ path: i.path?.map(String), message: i.message })),
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Thrown when config file loading or validation fails.
106
+ */
107
+ export class ConfigError extends PadroneError {
108
+ constructor(message: string, options?: PadroneErrorOptions) {
109
+ super(message, { phase: 'config', ...options });
110
+ this.name = 'ConfigError';
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Thrown from user action handlers to surface structured errors with exit codes and suggestions.
116
+ * This is the primary error class users should throw from their command actions.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * throw new ActionError('Missing environment', {
121
+ * exitCode: 1,
122
+ * suggestions: ['Use --env production or --env staging'],
123
+ * });
124
+ * ```
125
+ */
126
+ export class ActionError extends PadroneError {
127
+ constructor(message: string, options?: PadroneErrorOptions) {
128
+ super(message, { phase: 'execute', ...options });
129
+ this.name = 'ActionError';
130
+ }
131
+ }
package/src/formatter.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { camelToKebab } from './args.ts';
1
2
  import { createColorizer } from './colorizer.ts';
2
3
 
3
4
  export type HelpFormat = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json';
@@ -10,7 +11,7 @@ export type HelpDetail = 'minimal' | 'standard' | 'full';
10
11
  /**
11
12
  * Information about a single positional argument.
12
13
  */
13
- export type HelpArgumentInfo = {
14
+ export type HelpPositionalInfo = {
14
15
  name: string;
15
16
  description?: string;
16
17
  optional: boolean;
@@ -19,26 +20,29 @@ export type HelpArgumentInfo = {
19
20
  };
20
21
 
21
22
  /**
22
- * Information about a single option/flag.
23
+ * Information about a single argument/flag.
23
24
  */
24
- export type HelpOptionInfo = {
25
+ export type HelpArgumentInfo = {
25
26
  name: string;
26
27
  description?: string;
27
28
  optional: boolean;
28
29
  default?: unknown;
29
30
  type?: string;
30
31
  enum?: string[];
32
+ /** Single-character short flags (shown as `-v`) */
33
+ flags?: string[];
34
+ /** Multi-character alternative long names (shown as `--dry-run`) */
31
35
  aliases?: string[];
32
36
  deprecated?: boolean | string;
33
37
  hidden?: boolean;
34
38
  examples?: unknown[];
35
- /** Environment variable(s) this option can be set from */
39
+ /** Environment variable(s) this arg can be set from */
36
40
  env?: string | string[];
37
- /** Whether this option is an array type (shown as <type...>) */
41
+ /** Whether this arg is an array type (shown as <type...>) */
38
42
  variadic?: boolean;
39
- /** Whether this option is a boolean (shown as --[no-]option) */
43
+ /** Whether this arg is a boolean (shown as --[no-]arg) */
40
44
  negatable?: boolean;
41
- /** Config file key that maps to this option */
45
+ /** Config file key that maps to this arg */
42
46
  configKey?: string;
43
47
  };
44
48
 
@@ -52,6 +56,16 @@ export type HelpSubcommandInfo = {
52
56
  aliases?: string[];
53
57
  deprecated?: boolean | string;
54
58
  hidden?: boolean;
59
+ hasSubcommands?: boolean;
60
+ };
61
+
62
+ /**
63
+ * Information about a built-in command/flag entry.
64
+ */
65
+ export type HelpBuiltinInfo = {
66
+ name: string;
67
+ description?: string;
68
+ sub?: { name: string; description?: string }[];
55
69
  };
56
70
 
57
71
  /**
@@ -75,15 +89,19 @@ export type HelpInfo = {
75
89
  usage: {
76
90
  command: string;
77
91
  hasSubcommands: boolean;
92
+ hasPositionals: boolean;
78
93
  hasArguments: boolean;
79
- hasOptions: boolean;
94
+ /** The name of the field that reads from stdin, if any. Shown as `[stdin > field]` in usage. */
95
+ stdinField?: string;
80
96
  };
81
97
  /** List of subcommands */
82
98
  subcommands?: HelpSubcommandInfo[];
83
99
  /** Positional arguments */
100
+ positionals?: HelpPositionalInfo[];
101
+ /** Arguments/flags (only visible ones, hidden filtered out) */
84
102
  arguments?: HelpArgumentInfo[];
85
- /** Options/flags (only visible ones, hidden filtered out) */
86
- options?: HelpOptionInfo[];
103
+ /** Built-in commands and flags (shown only for root command) */
104
+ builtins?: HelpBuiltinInfo[];
87
105
  /** Full help info for nested commands (used in 'full' detail mode) */
88
106
  nestedCommands?: HelpInfo[];
89
107
  };
@@ -110,7 +128,7 @@ export type Formatter = {
110
128
  */
111
129
  type Styler = {
112
130
  command: (text: string) => string;
113
- option: (text: string) => string;
131
+ arg: (text: string) => string;
114
132
  type: (text: string) => string;
115
133
  description: (text: string) => string;
116
134
  label: (text: string) => string;
@@ -138,7 +156,7 @@ type LayoutConfig = {
138
156
  function createTextStyler(): Styler {
139
157
  return {
140
158
  command: (text) => text,
141
- option: (text) => text,
159
+ arg: (text) => text,
142
160
  type: (text) => text,
143
161
  description: (text) => text,
144
162
  label: (text) => text,
@@ -153,7 +171,7 @@ function createAnsiStyler(): Styler {
153
171
  const colorizer = createColorizer();
154
172
  return {
155
173
  command: colorizer.command,
156
- option: colorizer.option,
174
+ arg: colorizer.arg,
157
175
  type: colorizer.type,
158
176
  description: colorizer.description,
159
177
  label: colorizer.label,
@@ -179,7 +197,7 @@ function createConsoleStyler(): Styler {
179
197
  };
180
198
  return {
181
199
  command: (text) => `${colors.cyan}${colors.bold}${text}${colors.reset}`,
182
- option: (text) => `${colors.green}${text}${colors.reset}`,
200
+ arg: (text) => `${colors.green}${text}${colors.reset}`,
183
201
  type: (text) => `${colors.yellow}${text}${colors.reset}`,
184
202
  description: (text) => `${colors.dim}${text}${colors.reset}`,
185
203
  label: (text) => `${colors.bold}${text}${colors.reset}`,
@@ -193,7 +211,7 @@ function createConsoleStyler(): Styler {
193
211
  function createMarkdownStyler(): Styler {
194
212
  return {
195
213
  command: (text) => `**${text}**`,
196
- option: (text) => `\`${text}\``,
214
+ arg: (text) => `\`${text}\``,
197
215
  type: (text) => `\`${text}\``,
198
216
  description: (text) => text,
199
217
  label: (text) => `### ${text}`,
@@ -211,7 +229,7 @@ function escapeHtml(text: string): string {
211
229
  function createHtmlStyler(): Styler {
212
230
  return {
213
231
  command: (text) => `<strong style="color: #00bcd4;">${escapeHtml(text)}</strong>`,
214
- option: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
232
+ arg: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
215
233
  type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
216
234
  description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
217
235
  label: (text) => `<h3>${escapeHtml(text)}</h3>`,
@@ -269,12 +287,15 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
269
287
  const { newline, indent, join, wrapDocument, usageLabel } = layout;
270
288
 
271
289
  function formatUsageSection(info: HelpInfo): string[] {
272
- const usageParts: string[] = [
273
- styler.command(info.usage.command),
274
- info.usage.hasSubcommands ? styler.meta('[command]') : '',
275
- info.usage.hasArguments ? styler.meta('[args...]') : '',
276
- info.usage.hasOptions ? styler.meta('[options]') : '',
277
- ];
290
+ const usageParts: string[] = [styler.command(info.usage.command), info.usage.hasSubcommands ? styler.meta('[command]') : ''];
291
+ // Show actual positional argument names in usage line
292
+ if (info.positionals && info.positionals.length > 0) {
293
+ for (const arg of info.positionals) {
294
+ const name = arg.name.startsWith('...') ? `${arg.name}` : arg.name;
295
+ usageParts.push(styler.meta(arg.optional ? `[${name}]` : `<${name}>`));
296
+ }
297
+ }
298
+ if (info.usage.hasArguments) usageParts.push(styler.meta('[options]'));
278
299
  return [`${usageLabel} ${join(usageParts)}`];
279
300
  }
280
301
 
@@ -284,18 +305,40 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
284
305
 
285
306
  lines.push(styler.label('Commands:'));
286
307
 
308
+ const subcommandSuffix = (c: HelpSubcommandInfo) => (c.hasSubcommands ? ' <subcommand>' : '');
309
+ const formatAliasParts = (c: HelpSubcommandInfo) => {
310
+ if (!c.aliases?.length) return { plain: '', styled: '' };
311
+ const realAliases = c.aliases.filter((a) => a !== '[default]');
312
+ const hasDefault = c.aliases.some((a) => a === '[default]');
313
+ const parts: string[] = [];
314
+ const styledParts: string[] = [];
315
+ if (realAliases.length) {
316
+ parts.push(`(${realAliases.join(', ')})`);
317
+ styledParts.push(`(${realAliases.join(', ')})`);
318
+ }
319
+ if (hasDefault) {
320
+ parts.push('[default]');
321
+ styledParts.push(styler.meta('[default]'));
322
+ }
323
+ return { plain: parts.length ? ` ${parts.join(' ')}` : '', styled: styledParts.length ? ` ${styledParts.join(' ')}` : '' };
324
+ };
287
325
  const maxNameLength = Math.max(
288
326
  ...subcommands.map((c) => {
289
- const aliases = c.aliases ? ` (${c.aliases.join(', ')})` : '';
290
- return (c.name + aliases).length;
327
+ return (c.name + subcommandSuffix(c) + formatAliasParts(c).plain).length;
291
328
  }),
292
329
  );
293
330
  for (const subCmd of subcommands) {
294
- const aliases = subCmd.aliases ? ` (${subCmd.aliases.join(', ')})` : '';
295
- const commandDisplay = subCmd.name + aliases;
331
+ const aliasParts = formatAliasParts(subCmd);
332
+ const suffix = subcommandSuffix(subCmd);
333
+ const commandDisplay = subCmd.name + suffix + aliasParts.plain;
296
334
  const padding = ' '.repeat(Math.max(0, maxNameLength - commandDisplay.length + 2));
297
335
  const isDeprecated = !!subCmd.deprecated;
298
- const commandName = isDeprecated ? styler.deprecated(commandDisplay) : styler.command(subCmd.name) + aliases;
336
+ const isDefaultEntry = subCmd.name === '[default]';
337
+ const commandName = isDeprecated
338
+ ? styler.deprecated(commandDisplay)
339
+ : (isDefaultEntry ? styler.meta(subCmd.name) : styler.command(subCmd.name)) +
340
+ (suffix ? styler.meta(suffix) : '') +
341
+ aliasParts.styled;
299
342
  const lineParts: string[] = [commandName, padding];
300
343
 
301
344
  // Use title if available, otherwise use description
@@ -317,74 +360,101 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
317
360
  return lines;
318
361
  }
319
362
 
320
- function formatArgumentsSection(info: HelpInfo): string[] {
363
+ function formatPositionalsSection(info: HelpInfo): string[] {
321
364
  const lines: string[] = [];
322
- const args = info.arguments!;
365
+ const args = info.positionals!;
323
366
 
324
367
  lines.push(styler.label('Arguments:'));
325
368
 
326
- for (const arg of args) {
327
- const parts: string[] = [styler.option(arg.name)];
328
- if (arg.optional) parts.push(styler.meta('(optional)'));
329
- if (arg.default !== undefined) parts.push(styler.meta(`(default: ${String(arg.default)})`));
330
- lines.push(indent(1) + join(parts));
369
+ const maxNameLength = Math.min(32, Math.max(...args.map((a) => a.name.length)));
331
370
 
332
- if (arg.description) {
333
- lines.push(indent(2) + styler.description(arg.description));
334
- }
371
+ for (const arg of args) {
372
+ const padding = ' '.repeat(Math.max(2, maxNameLength - arg.name.length + 2));
373
+ const descParts: string[] = [];
374
+ if (arg.description) descParts.push(styler.description(arg.description));
375
+ if (info.usage.stdinField === arg.name) descParts.push(styler.meta('(stdin)'));
376
+ if (arg.default !== undefined) descParts.push(styler.meta(`(default: ${String(arg.default)})`));
377
+ lines.push(indent(1) + styler.arg(arg.name) + padding + join(descParts));
335
378
  }
336
379
 
337
380
  return lines;
338
381
  }
339
382
 
340
- function formatOptionsSection(info: HelpInfo): string[] {
383
+ function formatArgumentsSection(info: HelpInfo): string[] {
341
384
  const lines: string[] = [];
342
- const options = info.options!;
385
+ const argList = info.arguments || [];
343
386
 
344
387
  lines.push(styler.label('Options:'));
345
388
 
346
- const maxNameLength = Math.max(...options.map((opt) => opt.name.length));
347
-
348
- for (const opt of options) {
349
- // Format option name: --[no-]option for booleans, --option otherwise
350
- const optionName = opt.negatable ? `--[no-]${opt.name}` : `--${opt.name}`;
351
- const aliasNames = opt.aliases && opt.aliases.length > 0 ? opt.aliases.map((a) => `-${a}`).join(', ') : '';
352
- const fullOptionName = aliasNames ? `${optionName}, ${aliasNames}` : optionName;
353
- const padding = ' '.repeat(Math.max(0, maxNameLength - opt.name.length + 2));
354
- const isDeprecated = !!opt.deprecated;
355
- const formattedOptionName = isDeprecated ? styler.deprecated(fullOptionName) : styler.option(fullOptionName);
356
-
357
- const parts: string[] = [formattedOptionName];
358
- if (opt.type) parts.push(styler.type(`<${opt.type}>`));
359
- if (opt.optional && !opt.deprecated) parts.push(styler.meta('(optional)'));
360
- if (opt.default !== undefined) parts.push(styler.meta(`(default: ${String(opt.default)})`));
361
- if (opt.enum) parts.push(styler.meta(`(choices: ${opt.enum.join(', ')})`));
362
- if (opt.variadic) parts.push(styler.meta('(repeatable)'));
389
+ // Helper to check if a default value is meaningful (not empty string/array)
390
+ const hasDefault = (value: unknown): boolean => {
391
+ if (value === undefined) return false;
392
+ if (value === '') return false;
393
+ if (Array.isArray(value) && value.length === 0) return false;
394
+ return true;
395
+ };
396
+
397
+ // Build left column (signature) for each arg to compute alignment
398
+ const argColumns: { plain: string; styled: string; arg: HelpArgumentInfo }[] = argList.map((arg) => {
399
+ // Promote kebab-case alias to primary display name if it exists
400
+ const kebab = camelToKebab(arg.name);
401
+ const primaryName = kebab && arg.aliases?.includes(kebab) ? kebab : arg.name;
402
+ const remainingAliases = arg.aliases?.filter((a) => a !== primaryName);
403
+ const argName = `--${primaryName}`;
404
+ const flagNames = arg.flags?.length ? arg.flags.map((f) => `-${f}`).join(', ') : '';
405
+ const aliasNames = remainingAliases?.length ? remainingAliases.map((a) => `--${a}`).join(', ') : '';
406
+ const shortNames = [flagNames, aliasNames].filter(Boolean).join(', ');
407
+ const fullArgName = shortNames ? `${argName}, ${shortNames}` : argName;
408
+ const isDeprecated = !!arg.deprecated;
409
+ const formattedArgName = isDeprecated ? styler.deprecated(fullArgName) : styler.arg(fullArgName);
410
+
411
+ const plainParts: string[] = [fullArgName];
412
+ const styledParts: string[] = [formattedArgName];
413
+
414
+ if (arg.type && arg.type !== 'boolean') {
415
+ const typePart = arg.optional ? `[${arg.type}]` : `<${arg.type}>`;
416
+ plainParts.push(typePart);
417
+ styledParts.push(styler.type(typePart));
418
+ }
363
419
  if (isDeprecated) {
364
- const deprecatedMeta =
365
- typeof opt.deprecated === 'string' ? styler.meta(`(deprecated: ${opt.deprecated})`) : styler.meta('(deprecated)');
366
- parts.push(deprecatedMeta);
420
+ const deprecatedPart = typeof arg.deprecated === 'string' ? `(deprecated: ${arg.deprecated})` : '(deprecated)';
421
+ plainParts.push(deprecatedPart);
422
+ styledParts.push(styler.meta(deprecatedPart));
367
423
  }
368
424
 
369
- const description = opt.description ? styler.description(opt.description) : '';
370
- lines.push(indent(1) + join(parts) + padding + description);
425
+ return { plain: plainParts.join(' '), styled: join(styledParts), arg };
426
+ });
427
+
428
+ const maxColumnWidth = Math.min(32, Math.max(...argColumns.map((c) => c.plain.length)));
429
+
430
+ for (const { plain, styled, arg } of argColumns) {
431
+ const padding = ' '.repeat(Math.max(2, maxColumnWidth - plain.length + 2));
432
+
433
+ // Description followed by metadata (choices, default)
434
+ const descParts: string[] = [];
435
+ if (arg.description) descParts.push(styler.description(arg.description));
436
+ if (info.usage.stdinField === arg.name) descParts.push(styler.meta('(stdin)'));
437
+ if (arg.enum) descParts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
438
+ if (hasDefault(arg.default)) descParts.push(styler.meta(`(default: ${String(arg.default)})`));
439
+
440
+ lines.push(indent(1) + styled + padding + join(descParts));
371
441
 
372
442
  // Environment variable line
373
- if (opt.env) {
374
- const envVars = typeof opt.env === 'string' ? [opt.env] : opt.env;
443
+ if (arg.env) {
444
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
375
445
  const envParts: string[] = [styler.example('Env:'), styler.exampleValue(envVars.join(', '))];
376
446
  lines.push(indent(3) + join(envParts));
377
447
  }
378
448
 
379
449
  // Config key line
380
- if (opt.configKey) {
381
- const configParts: string[] = [styler.example('Config:'), styler.exampleValue(opt.configKey)];
450
+ if (arg.configKey) {
451
+ const configParts: string[] = [styler.example('Config:'), styler.exampleValue(arg.configKey)];
382
452
  lines.push(indent(3) + join(configParts));
383
453
  }
384
454
 
385
455
  // Examples line
386
- if (opt.examples && opt.examples.length > 0) {
387
- const exampleValues = opt.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
456
+ if (arg.examples && arg.examples.length > 0) {
457
+ const exampleValues = arg.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
388
458
  const exampleParts: string[] = [styler.example('Example:'), styler.exampleValue(exampleValues)];
389
459
  lines.push(indent(3) + join(exampleParts));
390
460
  }
@@ -393,6 +463,44 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
393
463
  return lines;
394
464
  }
395
465
 
466
+ function formatBuiltinsSection(info: HelpInfo): string[] {
467
+ const lines: string[] = [];
468
+ const builtins = info.builtins!;
469
+
470
+ lines.push(styler.label('Built-in:'));
471
+
472
+ // Compute max effective name length for alignment across main and sub entries
473
+ const allLengths: number[] = [];
474
+ for (const entry of builtins) {
475
+ allLengths.push(entry.name.length);
476
+ if (entry.sub) {
477
+ for (const sub of entry.sub) {
478
+ // Sub entries get extra indent(2) - indent(1) = 2 chars
479
+ allLengths.push(sub.name.length + 2);
480
+ }
481
+ }
482
+ }
483
+ const maxLen = Math.max(...allLengths);
484
+
485
+ for (const entry of builtins) {
486
+ const padding = ' '.repeat(Math.max(2, maxLen - entry.name.length + 2));
487
+ const parts: string[] = [styler.command(entry.name)];
488
+ if (entry.description) parts.push(padding + styler.description(entry.description));
489
+ lines.push(indent(1) + parts.join(''));
490
+
491
+ if (entry.sub) {
492
+ for (const sub of entry.sub) {
493
+ const subPadding = ' '.repeat(Math.max(2, maxLen - sub.name.length));
494
+ const subParts: string[] = [styler.arg(sub.name)];
495
+ if (sub.description) subParts.push(subPadding + styler.description(sub.description));
496
+ lines.push(indent(2) + subParts.join(''));
497
+ }
498
+ }
499
+ }
500
+
501
+ return lines;
502
+ }
503
+
396
504
  return {
397
505
  format(info: HelpInfo): string {
398
506
  const lines: string[] = [];
@@ -433,15 +541,24 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
433
541
  lines.push('');
434
542
  }
435
543
 
436
- // Arguments section
544
+ if (info.positionals && info.positionals.length > 0) {
545
+ lines.push(...formatPositionalsSection(info));
546
+ lines.push('');
547
+ }
548
+
437
549
  if (info.arguments && info.arguments.length > 0) {
438
550
  lines.push(...formatArgumentsSection(info));
439
551
  lines.push('');
440
552
  }
441
553
 
442
- // Options section
443
- if (info.options && info.options.length > 0) {
444
- lines.push(...formatOptionsSection(info));
554
+ if (info.builtins && info.builtins.length > 0) {
555
+ lines.push(...formatBuiltinsSection(info));
556
+ lines.push('');
557
+ }
558
+
559
+ // Show --no- hint when there are negatable boolean options defaulting to true
560
+ if (info.arguments?.some((arg) => arg.negatable && arg.default === true)) {
561
+ lines.push(styler.meta('Boolean options can be negated with --no-<option>.'));
445
562
  lines.push('');
446
563
  }
447
564
 
@@ -497,8 +614,13 @@ function createMinimalFormatter(): Formatter {
497
614
  format(info: HelpInfo): string {
498
615
  const parts: string[] = [info.usage.command];
499
616
  if (info.usage.hasSubcommands) parts.push('[command]');
500
- if (info.usage.hasArguments) parts.push('[args...]');
501
- if (info.usage.hasOptions) parts.push('[options]');
617
+ if (info.positionals && info.positionals.length > 0) {
618
+ for (const arg of info.positionals) {
619
+ const name = arg.name.startsWith('...') ? `${arg.name}` : arg.name;
620
+ parts.push(arg.optional ? `[${name}]` : `<${name}>`);
621
+ }
622
+ }
623
+ if (info.usage.hasArguments) parts.push('[options]');
502
624
  return parts.join(' ');
503
625
  },
504
626
  };