padrone 1.0.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 +51 -0
  2. package/LICENSE +1 -1
  3. package/README.md +92 -49
  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 +122 -438
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1240 -1161
  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 -20
  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 +1044 -284
  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 +13 -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 +12 -12
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +803 -144
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +185 -0
  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
@@ -10,7 +10,7 @@ export type HelpDetail = 'minimal' | 'standard' | 'full';
10
10
  /**
11
11
  * Information about a single positional argument.
12
12
  */
13
- export type HelpArgumentInfo = {
13
+ export type HelpPositionalInfo = {
14
14
  name: string;
15
15
  description?: string;
16
16
  optional: boolean;
@@ -19,9 +19,9 @@ export type HelpArgumentInfo = {
19
19
  };
20
20
 
21
21
  /**
22
- * Information about a single option/flag.
22
+ * Information about a single argument/flag.
23
23
  */
24
- export type HelpOptionInfo = {
24
+ export type HelpArgumentInfo = {
25
25
  name: string;
26
26
  description?: string;
27
27
  optional: boolean;
@@ -32,13 +32,13 @@ export type HelpOptionInfo = {
32
32
  deprecated?: boolean | string;
33
33
  hidden?: boolean;
34
34
  examples?: unknown[];
35
- /** Environment variable(s) this option can be set from */
35
+ /** Environment variable(s) this arg can be set from */
36
36
  env?: string | string[];
37
- /** Whether this option is an array type (shown as <type...>) */
37
+ /** Whether this arg is an array type (shown as <type...>) */
38
38
  variadic?: boolean;
39
- /** Whether this option is a boolean (shown as --[no-]option) */
39
+ /** Whether this arg is a boolean (shown as --[no-]arg) */
40
40
  negatable?: boolean;
41
- /** Config file key that maps to this option */
41
+ /** Config file key that maps to this arg */
42
42
  configKey?: string;
43
43
  };
44
44
 
@@ -52,6 +52,16 @@ export type HelpSubcommandInfo = {
52
52
  aliases?: string[];
53
53
  deprecated?: boolean | string;
54
54
  hidden?: boolean;
55
+ hasSubcommands?: boolean;
56
+ };
57
+
58
+ /**
59
+ * Information about a built-in command/flag entry.
60
+ */
61
+ export type HelpBuiltinInfo = {
62
+ name: string;
63
+ description?: string;
64
+ sub?: { name: string; description?: string }[];
55
65
  };
56
66
 
57
67
  /**
@@ -75,15 +85,19 @@ export type HelpInfo = {
75
85
  usage: {
76
86
  command: string;
77
87
  hasSubcommands: boolean;
88
+ hasPositionals: boolean;
78
89
  hasArguments: boolean;
79
- hasOptions: boolean;
90
+ /** The name of the field that reads from stdin, if any. Shown as `[stdin > field]` in usage. */
91
+ stdinField?: string;
80
92
  };
81
93
  /** List of subcommands */
82
94
  subcommands?: HelpSubcommandInfo[];
83
95
  /** Positional arguments */
96
+ positionals?: HelpPositionalInfo[];
97
+ /** Arguments/flags (only visible ones, hidden filtered out) */
84
98
  arguments?: HelpArgumentInfo[];
85
- /** Options/flags (only visible ones, hidden filtered out) */
86
- options?: HelpOptionInfo[];
99
+ /** Built-in commands and flags (shown only for root command) */
100
+ builtins?: HelpBuiltinInfo[];
87
101
  /** Full help info for nested commands (used in 'full' detail mode) */
88
102
  nestedCommands?: HelpInfo[];
89
103
  };
@@ -110,7 +124,7 @@ export type Formatter = {
110
124
  */
111
125
  type Styler = {
112
126
  command: (text: string) => string;
113
- option: (text: string) => string;
127
+ arg: (text: string) => string;
114
128
  type: (text: string) => string;
115
129
  description: (text: string) => string;
116
130
  label: (text: string) => string;
@@ -138,7 +152,7 @@ type LayoutConfig = {
138
152
  function createTextStyler(): Styler {
139
153
  return {
140
154
  command: (text) => text,
141
- option: (text) => text,
155
+ arg: (text) => text,
142
156
  type: (text) => text,
143
157
  description: (text) => text,
144
158
  label: (text) => text,
@@ -153,7 +167,7 @@ function createAnsiStyler(): Styler {
153
167
  const colorizer = createColorizer();
154
168
  return {
155
169
  command: colorizer.command,
156
- option: colorizer.option,
170
+ arg: colorizer.arg,
157
171
  type: colorizer.type,
158
172
  description: colorizer.description,
159
173
  label: colorizer.label,
@@ -179,7 +193,7 @@ function createConsoleStyler(): Styler {
179
193
  };
180
194
  return {
181
195
  command: (text) => `${colors.cyan}${colors.bold}${text}${colors.reset}`,
182
- option: (text) => `${colors.green}${text}${colors.reset}`,
196
+ arg: (text) => `${colors.green}${text}${colors.reset}`,
183
197
  type: (text) => `${colors.yellow}${text}${colors.reset}`,
184
198
  description: (text) => `${colors.dim}${text}${colors.reset}`,
185
199
  label: (text) => `${colors.bold}${text}${colors.reset}`,
@@ -193,7 +207,7 @@ function createConsoleStyler(): Styler {
193
207
  function createMarkdownStyler(): Styler {
194
208
  return {
195
209
  command: (text) => `**${text}**`,
196
- option: (text) => `\`${text}\``,
210
+ arg: (text) => `\`${text}\``,
197
211
  type: (text) => `\`${text}\``,
198
212
  description: (text) => text,
199
213
  label: (text) => `### ${text}`,
@@ -211,7 +225,7 @@ function escapeHtml(text: string): string {
211
225
  function createHtmlStyler(): Styler {
212
226
  return {
213
227
  command: (text) => `<strong style="color: #00bcd4;">${escapeHtml(text)}</strong>`,
214
- option: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
228
+ arg: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
215
229
  type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
216
230
  description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
217
231
  label: (text) => `<h3>${escapeHtml(text)}</h3>`,
@@ -269,12 +283,16 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
269
283
  const { newline, indent, join, wrapDocument, usageLabel } = layout;
270
284
 
271
285
  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
- ];
286
+ const usageParts: string[] = [styler.command(info.usage.command), info.usage.hasSubcommands ? styler.meta('[command]') : ''];
287
+ // Show actual positional argument names in usage line
288
+ if (info.positionals && info.positionals.length > 0) {
289
+ for (const arg of info.positionals) {
290
+ const name = arg.name.startsWith('...') ? `${arg.name}` : arg.name;
291
+ usageParts.push(styler.meta(arg.optional ? `[${name}]` : `<${name}>`));
292
+ }
293
+ }
294
+ if (info.usage.hasArguments) usageParts.push(styler.meta('[options]'));
295
+ if (info.usage.stdinField) usageParts.push(styler.meta(`[stdin > ${info.usage.stdinField}]`));
278
296
  return [`${usageLabel} ${join(usageParts)}`];
279
297
  }
280
298
 
@@ -284,18 +302,40 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
284
302
 
285
303
  lines.push(styler.label('Commands:'));
286
304
 
305
+ const subcommandSuffix = (c: HelpSubcommandInfo) => (c.hasSubcommands ? ' <subcommand>' : '');
306
+ const formatAliasParts = (c: HelpSubcommandInfo) => {
307
+ if (!c.aliases?.length) return { plain: '', styled: '' };
308
+ const realAliases = c.aliases.filter((a) => a !== '[default]');
309
+ const hasDefault = c.aliases.some((a) => a === '[default]');
310
+ const parts: string[] = [];
311
+ const styledParts: string[] = [];
312
+ if (realAliases.length) {
313
+ parts.push(`(${realAliases.join(', ')})`);
314
+ styledParts.push(`(${realAliases.join(', ')})`);
315
+ }
316
+ if (hasDefault) {
317
+ parts.push('[default]');
318
+ styledParts.push(styler.meta('[default]'));
319
+ }
320
+ return { plain: parts.length ? ` ${parts.join(' ')}` : '', styled: styledParts.length ? ` ${styledParts.join(' ')}` : '' };
321
+ };
287
322
  const maxNameLength = Math.max(
288
323
  ...subcommands.map((c) => {
289
- const aliases = c.aliases ? ` (${c.aliases.join(', ')})` : '';
290
- return (c.name + aliases).length;
324
+ return (c.name + subcommandSuffix(c) + formatAliasParts(c).plain).length;
291
325
  }),
292
326
  );
293
327
  for (const subCmd of subcommands) {
294
- const aliases = subCmd.aliases ? ` (${subCmd.aliases.join(', ')})` : '';
295
- const commandDisplay = subCmd.name + aliases;
328
+ const aliasParts = formatAliasParts(subCmd);
329
+ const suffix = subcommandSuffix(subCmd);
330
+ const commandDisplay = subCmd.name + suffix + aliasParts.plain;
296
331
  const padding = ' '.repeat(Math.max(0, maxNameLength - commandDisplay.length + 2));
297
332
  const isDeprecated = !!subCmd.deprecated;
298
- const commandName = isDeprecated ? styler.deprecated(commandDisplay) : styler.command(subCmd.name) + aliases;
333
+ const isDefaultEntry = subCmd.name === '[default]';
334
+ const commandName = isDeprecated
335
+ ? styler.deprecated(commandDisplay)
336
+ : (isDefaultEntry ? styler.meta(subCmd.name) : styler.command(subCmd.name)) +
337
+ (suffix ? styler.meta(suffix) : '') +
338
+ aliasParts.styled;
299
339
  const lineParts: string[] = [commandName, padding];
300
340
 
301
341
  // Use title if available, otherwise use description
@@ -317,14 +357,14 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
317
357
  return lines;
318
358
  }
319
359
 
320
- function formatArgumentsSection(info: HelpInfo): string[] {
360
+ function formatPositionalsSection(info: HelpInfo): string[] {
321
361
  const lines: string[] = [];
322
- const args = info.arguments!;
362
+ const args = info.positionals!;
323
363
 
324
364
  lines.push(styler.label('Arguments:'));
325
365
 
326
366
  for (const arg of args) {
327
- const parts: string[] = [styler.option(arg.name)];
367
+ const parts: string[] = [styler.arg(arg.name)];
328
368
  if (arg.optional) parts.push(styler.meta('(optional)'));
329
369
  if (arg.default !== undefined) parts.push(styler.meta(`(default: ${String(arg.default)})`));
330
370
  lines.push(indent(1) + join(parts));
@@ -337,54 +377,54 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
337
377
  return lines;
338
378
  }
339
379
 
340
- function formatOptionsSection(info: HelpInfo): string[] {
380
+ function formatArgumentsSection(info: HelpInfo): string[] {
341
381
  const lines: string[] = [];
342
- const options = info.options!;
382
+ const argList = info.arguments || [];
343
383
 
344
384
  lines.push(styler.label('Options:'));
345
385
 
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)'));
386
+ const maxNameLength = Math.max(...argList.map((arg) => arg.name.length));
387
+
388
+ for (const arg of argList) {
389
+ // Format arg name: --[no-]arg for booleans, --arg otherwise
390
+ const argName = arg.negatable ? `--[no-]${arg.name}` : `--${arg.name}`;
391
+ const aliasNames = arg.aliases && arg.aliases.length > 0 ? arg.aliases.map((a) => `-${a}`).join(', ') : '';
392
+ const fullArgName = aliasNames ? `${argName}, ${aliasNames}` : argName;
393
+ const padding = ' '.repeat(Math.max(0, maxNameLength - arg.name.length + 2));
394
+ const isDeprecated = !!arg.deprecated;
395
+ const formattedArgName = isDeprecated ? styler.deprecated(fullArgName) : styler.arg(fullArgName);
396
+
397
+ const parts: string[] = [formattedArgName];
398
+ if (arg.type) parts.push(styler.type(`<${arg.type}>`));
399
+ if (arg.optional && !arg.deprecated) parts.push(styler.meta('(optional)'));
400
+ if (arg.default !== undefined) parts.push(styler.meta(`(default: ${String(arg.default)})`));
401
+ if (arg.enum) parts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
402
+ if (arg.variadic) parts.push(styler.meta('(repeatable)'));
363
403
  if (isDeprecated) {
364
404
  const deprecatedMeta =
365
- typeof opt.deprecated === 'string' ? styler.meta(`(deprecated: ${opt.deprecated})`) : styler.meta('(deprecated)');
405
+ typeof arg.deprecated === 'string' ? styler.meta(`(deprecated: ${arg.deprecated})`) : styler.meta('(deprecated)');
366
406
  parts.push(deprecatedMeta);
367
407
  }
368
408
 
369
- const description = opt.description ? styler.description(opt.description) : '';
409
+ const description = arg.description ? styler.description(arg.description) : '';
370
410
  lines.push(indent(1) + join(parts) + padding + description);
371
411
 
372
412
  // Environment variable line
373
- if (opt.env) {
374
- const envVars = typeof opt.env === 'string' ? [opt.env] : opt.env;
413
+ if (arg.env) {
414
+ const envVars = typeof arg.env === 'string' ? [arg.env] : arg.env;
375
415
  const envParts: string[] = [styler.example('Env:'), styler.exampleValue(envVars.join(', '))];
376
416
  lines.push(indent(3) + join(envParts));
377
417
  }
378
418
 
379
419
  // Config key line
380
- if (opt.configKey) {
381
- const configParts: string[] = [styler.example('Config:'), styler.exampleValue(opt.configKey)];
420
+ if (arg.configKey) {
421
+ const configParts: string[] = [styler.example('Config:'), styler.exampleValue(arg.configKey)];
382
422
  lines.push(indent(3) + join(configParts));
383
423
  }
384
424
 
385
425
  // 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(', ');
426
+ if (arg.examples && arg.examples.length > 0) {
427
+ const exampleValues = arg.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
388
428
  const exampleParts: string[] = [styler.example('Example:'), styler.exampleValue(exampleValues)];
389
429
  lines.push(indent(3) + join(exampleParts));
390
430
  }
@@ -393,6 +433,44 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
393
433
  return lines;
394
434
  }
395
435
 
436
+ function formatBuiltinsSection(info: HelpInfo): string[] {
437
+ const lines: string[] = [];
438
+ const builtins = info.builtins!;
439
+
440
+ lines.push(styler.label('Built-in:'));
441
+
442
+ // Compute max effective name length for alignment across main and sub entries
443
+ const allLengths: number[] = [];
444
+ for (const entry of builtins) {
445
+ allLengths.push(entry.name.length);
446
+ if (entry.sub) {
447
+ for (const sub of entry.sub) {
448
+ // Sub entries get extra indent(2) - indent(1) = 2 chars
449
+ allLengths.push(sub.name.length + 2);
450
+ }
451
+ }
452
+ }
453
+ const maxLen = Math.max(...allLengths);
454
+
455
+ for (const entry of builtins) {
456
+ const padding = ' '.repeat(Math.max(2, maxLen - entry.name.length + 2));
457
+ const parts: string[] = [styler.command(entry.name)];
458
+ if (entry.description) parts.push(padding + styler.description(entry.description));
459
+ lines.push(indent(1) + parts.join(''));
460
+
461
+ if (entry.sub) {
462
+ for (const sub of entry.sub) {
463
+ const subPadding = ' '.repeat(Math.max(2, maxLen - sub.name.length));
464
+ const subParts: string[] = [styler.arg(sub.name)];
465
+ if (sub.description) subParts.push(subPadding + styler.description(sub.description));
466
+ lines.push(indent(2) + subParts.join(''));
467
+ }
468
+ }
469
+ }
470
+
471
+ return lines;
472
+ }
473
+
396
474
  return {
397
475
  format(info: HelpInfo): string {
398
476
  const lines: string[] = [];
@@ -433,15 +511,18 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter
433
511
  lines.push('');
434
512
  }
435
513
 
436
- // Arguments section
514
+ if (info.positionals && info.positionals.length > 0) {
515
+ lines.push(...formatPositionalsSection(info));
516
+ lines.push('');
517
+ }
518
+
437
519
  if (info.arguments && info.arguments.length > 0) {
438
520
  lines.push(...formatArgumentsSection(info));
439
521
  lines.push('');
440
522
  }
441
523
 
442
- // Options section
443
- if (info.options && info.options.length > 0) {
444
- lines.push(...formatOptionsSection(info));
524
+ if (info.builtins && info.builtins.length > 0) {
525
+ lines.push(...formatBuiltinsSection(info));
445
526
  lines.push('');
446
527
  }
447
528
 
@@ -497,8 +578,13 @@ function createMinimalFormatter(): Formatter {
497
578
  format(info: HelpInfo): string {
498
579
  const parts: string[] = [info.usage.command];
499
580
  if (info.usage.hasSubcommands) parts.push('[command]');
500
- if (info.usage.hasArguments) parts.push('[args...]');
501
- if (info.usage.hasOptions) parts.push('[options]');
581
+ if (info.positionals && info.positionals.length > 0) {
582
+ for (const arg of info.positionals) {
583
+ const name = arg.name.startsWith('...') ? `${arg.name}` : arg.name;
584
+ parts.push(arg.optional ? `[${name}]` : `<${name}>`);
585
+ }
586
+ }
587
+ if (info.usage.hasArguments) parts.push('[options]');
502
588
  return parts.join(' ');
503
589
  },
504
590
  };