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.
- package/CHANGELOG.md +97 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-DFEI7_G_.mjs +197 -0
- package/dist/args-DFEI7_G_.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1358 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +405 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-XroimS3Q.d.mts +83 -0
- package/dist/formatter-XroimS3Q.d.mts.map +1 -0
- package/dist/help-CgGP7hQU.mjs +1229 -0
- package/dist/help-CgGP7hQU.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1220 -1204
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-BS7RP5Ls.d.mts +1059 -0
- package/dist/types-BS7RP5Ls.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -21
- package/src/args.ts +457 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +330 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +197 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +504 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1048 -308
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +195 -73
- package/src/help.ts +159 -58
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +52 -21
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +10 -10
- package/src/type-utils.ts +124 -14
- package/src/types.ts +752 -154
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +44 -40
- package/src/zod.d.ts +2 -2
- 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
|
|
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
|
|
23
|
+
* Information about a single argument/flag.
|
|
23
24
|
*/
|
|
24
|
-
export type
|
|
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
|
|
39
|
+
/** Environment variable(s) this arg can be set from */
|
|
36
40
|
env?: string | string[];
|
|
37
|
-
/** Whether this
|
|
41
|
+
/** Whether this arg is an array type (shown as <type...>) */
|
|
38
42
|
variadic?: boolean;
|
|
39
|
-
/** Whether this
|
|
43
|
+
/** Whether this arg is a boolean (shown as --[no-]arg) */
|
|
40
44
|
negatable?: boolean;
|
|
41
|
-
/** Config file key that maps to this
|
|
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
|
-
|
|
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
|
-
/**
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
295
|
-
const
|
|
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
|
|
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
|
|
363
|
+
function formatPositionalsSection(info: HelpInfo): string[] {
|
|
321
364
|
const lines: string[] = [];
|
|
322
|
-
const args = info.
|
|
365
|
+
const args = info.positionals!;
|
|
323
366
|
|
|
324
367
|
lines.push(styler.label('Arguments:'));
|
|
325
368
|
|
|
326
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
383
|
+
function formatArgumentsSection(info: HelpInfo): string[] {
|
|
341
384
|
const lines: string[] = [];
|
|
342
|
-
const
|
|
385
|
+
const argList = info.arguments || [];
|
|
343
386
|
|
|
344
387
|
lines.push(styler.label('Options:'));
|
|
345
388
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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 (
|
|
374
|
-
const envVars = typeof
|
|
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 (
|
|
381
|
-
const configParts: string[] = [styler.example('Config:'), styler.exampleValue(
|
|
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 (
|
|
387
|
-
const exampleValues =
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
lines.push(
|
|
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.
|
|
501
|
-
|
|
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
|
};
|