padrone 1.0.0-beta.2

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.
@@ -0,0 +1,499 @@
1
+ import { createColorizer } from './colorizer';
2
+
3
+ export type HelpFormat = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json';
4
+ export type HelpDetail = 'minimal' | 'standard' | 'full';
5
+
6
+ // ============================================================================
7
+ // Help Info Types (shared with help.ts)
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Information about a single positional argument.
12
+ */
13
+ export type HelpArgumentInfo = {
14
+ name: string;
15
+ description?: string;
16
+ optional: boolean;
17
+ default?: unknown;
18
+ type?: string;
19
+ };
20
+
21
+ /**
22
+ * Information about a single option/flag.
23
+ */
24
+ export type HelpOptionInfo = {
25
+ name: string;
26
+ description?: string;
27
+ optional: boolean;
28
+ default?: unknown;
29
+ type?: string;
30
+ enum?: string[];
31
+ aliases?: string[];
32
+ deprecated?: boolean | string;
33
+ hidden?: boolean;
34
+ examples?: unknown[];
35
+ /** Environment variable(s) this option can be set from */
36
+ env?: string | string[];
37
+ /** Whether this option is an array type (shown as <type...>) */
38
+ variadic?: boolean;
39
+ /** Whether this option is a boolean (shown as --[no-]option) */
40
+ negatable?: boolean;
41
+ /** Config file key that maps to this option */
42
+ configKey?: string;
43
+ };
44
+
45
+ /**
46
+ * Information about a subcommand (minimal info for listing).
47
+ */
48
+ export type HelpSubcommandInfo = {
49
+ name: string;
50
+ title?: string;
51
+ description?: string;
52
+ deprecated?: boolean | string;
53
+ hidden?: boolean;
54
+ };
55
+
56
+ /**
57
+ * Comprehensive JSON structure for help information.
58
+ * This is the single source of truth that all formatters use.
59
+ */
60
+ export type HelpInfo = {
61
+ /** The full command name (e.g., "cli serve" or "<root>") */
62
+ name: string;
63
+ /** Short title for the command */
64
+ title?: string;
65
+ /** Command description */
66
+ description?: string;
67
+ /** Whether the command is deprecated */
68
+ deprecated?: boolean | string;
69
+ /** Whether the command is hidden */
70
+ hidden?: boolean;
71
+ /** Usage string parts for flexible formatting */
72
+ usage: {
73
+ command: string;
74
+ hasSubcommands: boolean;
75
+ hasArguments: boolean;
76
+ hasOptions: boolean;
77
+ };
78
+ /** List of subcommands */
79
+ subcommands?: HelpSubcommandInfo[];
80
+ /** Positional arguments */
81
+ arguments?: HelpArgumentInfo[];
82
+ /** Options/flags (only visible ones, hidden filtered out) */
83
+ options?: HelpOptionInfo[];
84
+ /** Full help info for nested commands (used in 'full' detail mode) */
85
+ nestedCommands?: HelpInfo[];
86
+ };
87
+
88
+ // ============================================================================
89
+ // Formatter Interface
90
+ // ============================================================================
91
+
92
+ /**
93
+ * A formatter that takes the entire HelpInfo structure and produces formatted output.
94
+ */
95
+ export type Formatter = {
96
+ /** Format the entire help info structure into a string */
97
+ format: (info: HelpInfo) => string;
98
+ };
99
+
100
+ // ============================================================================
101
+ // Internal Styling Types
102
+ // ============================================================================
103
+
104
+ /**
105
+ * Internal styling functions used by formatters.
106
+ * These handle the visual styling of individual text elements.
107
+ */
108
+ type Styler = {
109
+ command: (text: string) => string;
110
+ option: (text: string) => string;
111
+ type: (text: string) => string;
112
+ description: (text: string) => string;
113
+ label: (text: string) => string;
114
+ meta: (text: string) => string;
115
+ example: (text: string) => string;
116
+ exampleValue: (text: string) => string;
117
+ deprecated: (text: string) => string;
118
+ };
119
+
120
+ /**
121
+ * Layout configuration for formatters.
122
+ */
123
+ type LayoutConfig = {
124
+ newline: string;
125
+ indent: (level: number) => string;
126
+ join: (parts: string[]) => string;
127
+ wrapDocument?: (content: string) => string;
128
+ usageLabel: string;
129
+ };
130
+
131
+ // ============================================================================
132
+ // Styler Factories
133
+ // ============================================================================
134
+
135
+ function createTextStyler(): Styler {
136
+ return {
137
+ command: (text) => text,
138
+ option: (text) => text,
139
+ type: (text) => text,
140
+ description: (text) => text,
141
+ label: (text) => text,
142
+ meta: (text) => text,
143
+ example: (text) => text,
144
+ exampleValue: (text) => text,
145
+ deprecated: (text) => text,
146
+ };
147
+ }
148
+
149
+ function createAnsiStyler(): Styler {
150
+ const colorizer = createColorizer();
151
+ return {
152
+ command: colorizer.command,
153
+ option: colorizer.option,
154
+ type: colorizer.type,
155
+ description: colorizer.description,
156
+ label: colorizer.label,
157
+ meta: colorizer.meta,
158
+ example: colorizer.example,
159
+ exampleValue: colorizer.exampleValue,
160
+ deprecated: colorizer.deprecated,
161
+ };
162
+ }
163
+
164
+ function createConsoleStyler(): Styler {
165
+ const colors = {
166
+ reset: '\x1b[0m',
167
+ bold: '\x1b[1m',
168
+ dim: '\x1b[2m',
169
+ italic: '\x1b[3m',
170
+ underline: '\x1b[4m',
171
+ strikethrough: '\x1b[9m',
172
+ cyan: '\x1b[36m',
173
+ green: '\x1b[32m',
174
+ yellow: '\x1b[33m',
175
+ gray: '\x1b[90m',
176
+ };
177
+ return {
178
+ command: (text) => `${colors.cyan}${colors.bold}${text}${colors.reset}`,
179
+ option: (text) => `${colors.green}${text}${colors.reset}`,
180
+ type: (text) => `${colors.yellow}${text}${colors.reset}`,
181
+ description: (text) => `${colors.dim}${text}${colors.reset}`,
182
+ label: (text) => `${colors.bold}${text}${colors.reset}`,
183
+ meta: (text) => `${colors.gray}${text}${colors.reset}`,
184
+ example: (text) => `${colors.underline}${text}${colors.reset}`,
185
+ exampleValue: (text) => `${colors.italic}${text}${colors.reset}`,
186
+ deprecated: (text) => `${colors.strikethrough}${colors.gray}${text}${colors.reset}`,
187
+ };
188
+ }
189
+
190
+ function createMarkdownStyler(): Styler {
191
+ return {
192
+ command: (text) => `**${text}**`,
193
+ option: (text) => `\`${text}\``,
194
+ type: (text) => `\`${text}\``,
195
+ description: (text) => text,
196
+ label: (text) => `### ${text}`,
197
+ meta: (text) => `*${text}*`,
198
+ example: (text) => `**${text}**`,
199
+ exampleValue: (text) => `\`${text}\``,
200
+ deprecated: (text) => `~~${text}~~`,
201
+ };
202
+ }
203
+
204
+ function escapeHtml(text: string): string {
205
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
206
+ }
207
+
208
+ function createHtmlStyler(): Styler {
209
+ return {
210
+ command: (text) => `<strong style="color: #00bcd4;">${escapeHtml(text)}</strong>`,
211
+ option: (text) => `<code style="color: #4caf50;">${escapeHtml(text)}</code>`,
212
+ type: (text) => `<code style="color: #ff9800;">${escapeHtml(text)}</code>`,
213
+ description: (text) => `<span style="color: #666;">${escapeHtml(text)}</span>`,
214
+ label: (text) => `<h3>${escapeHtml(text)}</h3>`,
215
+ meta: (text) => `<span style="color: #999;">${escapeHtml(text)}</span>`,
216
+ example: (text) => `<strong style="text-decoration: underline;">${escapeHtml(text)}</strong>`,
217
+ exampleValue: (text) => `<em>${escapeHtml(text)}</em>`,
218
+ deprecated: (text) => `<del style="color: #999;">${escapeHtml(text)}</del>`,
219
+ };
220
+ }
221
+
222
+ // ============================================================================
223
+ // Layout Configurations
224
+ // ============================================================================
225
+
226
+ function createTextLayout(): LayoutConfig {
227
+ return {
228
+ newline: '\n',
229
+ indent: (level) => ' '.repeat(level),
230
+ join: (parts) => parts.filter(Boolean).join(' '),
231
+ usageLabel: 'Usage:',
232
+ };
233
+ }
234
+
235
+ function createMarkdownLayout(): LayoutConfig {
236
+ return {
237
+ newline: '\n\n',
238
+ indent: (level) => {
239
+ if (level === 0) return '';
240
+ if (level === 1) return ' ';
241
+ return ' ';
242
+ },
243
+ join: (parts) => parts.filter(Boolean).join(' '),
244
+ usageLabel: 'Usage:',
245
+ };
246
+ }
247
+
248
+ function createHtmlLayout(): LayoutConfig {
249
+ return {
250
+ newline: '<br>',
251
+ indent: (level) => '&nbsp;&nbsp;'.repeat(level),
252
+ join: (parts) => parts.filter(Boolean).join(' '),
253
+ wrapDocument: (content) => `<div style="font-family: monospace; line-height: 1.6;">${content}</div>`,
254
+ usageLabel: '<strong>Usage:</strong>',
255
+ };
256
+ }
257
+
258
+ // ============================================================================
259
+ // Generic Formatter Implementation
260
+ // ============================================================================
261
+
262
+ /**
263
+ * Creates a formatter that uses the given styler and layout configuration.
264
+ */
265
+ function createGenericFormatter(styler: Styler, layout: LayoutConfig): Formatter {
266
+ const { newline, indent, join, wrapDocument, usageLabel } = layout;
267
+
268
+ function formatUsageSection(info: HelpInfo): string[] {
269
+ const usageParts: string[] = [
270
+ styler.command(info.usage.command),
271
+ info.usage.hasSubcommands ? styler.meta('[command]') : '',
272
+ info.usage.hasArguments ? styler.meta('[args...]') : '',
273
+ info.usage.hasOptions ? styler.meta('[options]') : '',
274
+ ];
275
+ return [`${usageLabel} ${join(usageParts)}`];
276
+ }
277
+
278
+ function formatSubcommandsSection(info: HelpInfo): string[] {
279
+ const lines: string[] = [];
280
+ const subcommands = info.subcommands!;
281
+
282
+ lines.push(styler.label('Commands:'));
283
+
284
+ const maxNameLength = Math.max(...subcommands.map((c) => c.name.length));
285
+ for (const subCmd of subcommands) {
286
+ const padding = ' '.repeat(Math.max(0, maxNameLength - subCmd.name.length + 2));
287
+ const isDeprecated = !!subCmd.deprecated;
288
+ const commandName = isDeprecated ? styler.deprecated(subCmd.name) : styler.command(subCmd.name);
289
+ const lineParts: string[] = [commandName, padding];
290
+
291
+ // Use title if available, otherwise use description
292
+ const displayText = subCmd.title ?? subCmd.description;
293
+ if (displayText) {
294
+ lineParts.push(isDeprecated ? styler.deprecated(displayText) : styler.description(displayText));
295
+ }
296
+ if (isDeprecated) {
297
+ const deprecatedMeta =
298
+ typeof subCmd.deprecated === 'string' ? styler.meta(` (deprecated: ${subCmd.deprecated})`) : styler.meta(' (deprecated)');
299
+ lineParts.push(deprecatedMeta);
300
+ }
301
+ lines.push(indent(1) + lineParts.join(''));
302
+ }
303
+
304
+ lines.push('');
305
+ lines.push(styler.meta(`Run "${info.name} [command] --help" for more information on a command.`));
306
+
307
+ return lines;
308
+ }
309
+
310
+ function formatArgumentsSection(info: HelpInfo): string[] {
311
+ const lines: string[] = [];
312
+ const args = info.arguments!;
313
+
314
+ lines.push(styler.label('Arguments:'));
315
+
316
+ for (const arg of args) {
317
+ const parts: string[] = [styler.option(arg.name)];
318
+ if (arg.optional) parts.push(styler.meta('(optional)'));
319
+ if (arg.default !== undefined) parts.push(styler.meta(`(default: ${String(arg.default)})`));
320
+ lines.push(indent(1) + join(parts));
321
+
322
+ if (arg.description) {
323
+ lines.push(indent(2) + styler.description(arg.description));
324
+ }
325
+ }
326
+
327
+ return lines;
328
+ }
329
+
330
+ function formatOptionsSection(info: HelpInfo): string[] {
331
+ const lines: string[] = [];
332
+ const options = info.options!;
333
+
334
+ lines.push(styler.label('Options:'));
335
+
336
+ const maxNameLength = Math.max(...options.map((opt) => opt.name.length));
337
+
338
+ for (const opt of options) {
339
+ // Format option name: --[no-]option for booleans, --option otherwise
340
+ const optionName = opt.negatable ? `--[no-]${opt.name}` : `--${opt.name}`;
341
+ const aliasNames = opt.aliases && opt.aliases.length > 0 ? opt.aliases.map((a) => `-${a}`).join(', ') : '';
342
+ const fullOptionName = aliasNames ? `${optionName}, ${aliasNames}` : optionName;
343
+ const padding = ' '.repeat(Math.max(0, maxNameLength - opt.name.length + 2));
344
+ const isDeprecated = !!opt.deprecated;
345
+ const formattedOptionName = isDeprecated ? styler.deprecated(fullOptionName) : styler.option(fullOptionName);
346
+
347
+ const parts: string[] = [formattedOptionName];
348
+ if (opt.type) parts.push(styler.type(`<${opt.type}>`));
349
+ if (opt.optional && !opt.deprecated) parts.push(styler.meta('(optional)'));
350
+ if (opt.default !== undefined) parts.push(styler.meta(`(default: ${String(opt.default)})`));
351
+ if (opt.enum) parts.push(styler.meta(`(choices: ${opt.enum.join(', ')})`));
352
+ if (opt.variadic) parts.push(styler.meta('(repeatable)'));
353
+ if (isDeprecated) {
354
+ const deprecatedMeta =
355
+ typeof opt.deprecated === 'string' ? styler.meta(`(deprecated: ${opt.deprecated})`) : styler.meta('(deprecated)');
356
+ parts.push(deprecatedMeta);
357
+ }
358
+
359
+ const description = opt.description ? styler.description(opt.description) : '';
360
+ lines.push(indent(1) + join(parts) + padding + description);
361
+
362
+ // Environment variable line
363
+ if (opt.env) {
364
+ const envVars = typeof opt.env === 'string' ? [opt.env] : opt.env;
365
+ const envParts: string[] = [styler.example('Env:'), styler.exampleValue(envVars.join(', '))];
366
+ lines.push(indent(3) + join(envParts));
367
+ }
368
+
369
+ // Config key line
370
+ if (opt.configKey) {
371
+ const configParts: string[] = [styler.example('Config:'), styler.exampleValue(opt.configKey)];
372
+ lines.push(indent(3) + join(configParts));
373
+ }
374
+
375
+ // Examples line
376
+ if (opt.examples && opt.examples.length > 0) {
377
+ const exampleValues = opt.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
378
+ const exampleParts: string[] = [styler.example('Example:'), styler.exampleValue(exampleValues)];
379
+ lines.push(indent(3) + join(exampleParts));
380
+ }
381
+ }
382
+
383
+ return lines;
384
+ }
385
+
386
+ return {
387
+ format(info: HelpInfo): string {
388
+ const lines: string[] = [];
389
+
390
+ // Show deprecation warning at the top if command is deprecated
391
+ if (info.deprecated) {
392
+ const deprecationMessage =
393
+ typeof info.deprecated === 'string' ? `⚠️ This command is deprecated: ${info.deprecated}` : '⚠️ This command is deprecated';
394
+ lines.push(styler.deprecated(deprecationMessage));
395
+ lines.push('');
396
+ }
397
+
398
+ // Usage section
399
+ lines.push(...formatUsageSection(info));
400
+ lines.push('');
401
+
402
+ // Title section (if present, shows a short summary line)
403
+ if (info.title) {
404
+ lines.push(styler.label(info.title));
405
+ lines.push('');
406
+ }
407
+
408
+ // Description section (if present)
409
+ if (info.description) {
410
+ lines.push(styler.description(info.description));
411
+ lines.push('');
412
+ }
413
+
414
+ // Subcommands section
415
+ if (info.subcommands && info.subcommands.length > 0) {
416
+ lines.push(...formatSubcommandsSection(info));
417
+ lines.push('');
418
+ }
419
+
420
+ // Arguments section
421
+ if (info.arguments && info.arguments.length > 0) {
422
+ lines.push(...formatArgumentsSection(info));
423
+ lines.push('');
424
+ }
425
+
426
+ // Options section
427
+ if (info.options && info.options.length > 0) {
428
+ lines.push(...formatOptionsSection(info));
429
+ lines.push('');
430
+ }
431
+
432
+ // Nested commands section (full detail mode)
433
+ if (info.nestedCommands?.length) {
434
+ lines.push(styler.label('Subcommand Details:'));
435
+ lines.push('');
436
+ for (const nestedCmd of info.nestedCommands) {
437
+ lines.push(styler.meta('─'.repeat(60)));
438
+ lines.push(this.format(nestedCmd));
439
+ }
440
+ }
441
+
442
+ const result = lines.join(newline);
443
+ return wrapDocument ? wrapDocument(result) : result;
444
+ },
445
+ };
446
+ }
447
+
448
+ // ============================================================================
449
+ // JSON Formatter
450
+ // ============================================================================
451
+
452
+ function createJsonFormatter(): Formatter {
453
+ return {
454
+ format(info: HelpInfo): string {
455
+ return JSON.stringify(info, null, 2);
456
+ },
457
+ };
458
+ }
459
+
460
+ // ============================================================================
461
+ // Formatter Factory
462
+ // ============================================================================
463
+
464
+ function shouldUseAnsi(): boolean {
465
+ if (typeof process === 'undefined') return false;
466
+ if (process.env.NO_COLOR) return false;
467
+ if (process.env.CI) return false;
468
+ if (process.stdout && typeof process.stdout.isTTY === 'boolean') return process.stdout.isTTY;
469
+ return false;
470
+ }
471
+
472
+ // ============================================================================
473
+ // Minimal Formatter
474
+ // ============================================================================
475
+
476
+ /**
477
+ * Creates a minimal formatter that outputs just a single-line usage string.
478
+ */
479
+ function createMinimalFormatter(): Formatter {
480
+ return {
481
+ format(info: HelpInfo): string {
482
+ const parts: string[] = [info.usage.command];
483
+ if (info.usage.hasSubcommands) parts.push('[command]');
484
+ if (info.usage.hasArguments) parts.push('[args...]');
485
+ if (info.usage.hasOptions) parts.push('[options]');
486
+ return parts.join(' ');
487
+ },
488
+ };
489
+ }
490
+
491
+ export function createFormatter(format: HelpFormat | 'auto', detail: HelpDetail = 'standard'): Formatter {
492
+ if (detail === 'minimal') return createMinimalFormatter();
493
+ if (format === 'json') return createJsonFormatter();
494
+ if (format === 'ansi' || (format === 'auto' && shouldUseAnsi())) return createGenericFormatter(createAnsiStyler(), createTextLayout());
495
+ if (format === 'console') return createGenericFormatter(createConsoleStyler(), createTextLayout());
496
+ if (format === 'markdown') return createGenericFormatter(createMarkdownStyler(), createMarkdownLayout());
497
+ if (format === 'html') return createGenericFormatter(createHtmlStyler(), createHtmlLayout());
498
+ return createGenericFormatter(createTextStyler(), createTextLayout());
499
+ }