@guanghechen/commander 4.2.0 → 4.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat: add fluent help examples and styled help renderer
8
+
3
9
  ## 4.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -202,6 +202,29 @@ new Command({ name: 'example', description: 'Option types demo' })
202
202
  })
203
203
  ```
204
204
 
205
+ ### Help Examples
206
+
207
+ ```typescript
208
+ import { Command } from '@guanghechen/commander'
209
+
210
+ const cli = new Command({ name: 'mycli', desc: 'My CLI tool' })
211
+
212
+ cli
213
+ .example('Initialize Project', 'init my-app', 'Create project scaffold')
214
+ .example('Watch Build', 'build --watch', 'Rebuild on file changes')
215
+ .action(() => {})
216
+
217
+ await cli.run({ argv: ['--help'], envs: process.env })
218
+ ```
219
+
220
+ `usage` 是相对当前 command path 的片段,help 中会自动补齐前缀,例如 `mycli build --watch`。
221
+
222
+ `--color` / `--no-color` 仅控制 help 文本的终端着色;
223
+ `--log-colorful` / `--no-log-colorful` 控制 `Reporter` 的日志着色。
224
+
225
+ 当环境变量 `NO_COLOR` 存在时,help 渲染默认视为 `--no-color`;
226
+ 显式传入 `--color` 可以覆盖这个默认值。
227
+
205
228
  ## Reference
206
229
 
207
230
  - [homepage][homepage]
package/lib/cjs/index.cjs CHANGED
@@ -24,6 +24,18 @@ function _interopNamespaceDefault(e) {
24
24
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
25
25
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
26
26
 
27
+ const TERMINAL_STYLE = {
28
+ bold: '\x1b[1m',
29
+ italic: '\x1b[3m',
30
+ underline: '\x1b[4m',
31
+ cyan: '\x1b[36m',
32
+ dim: '\x1b[2m',
33
+ reset: '\x1b[0m',
34
+ };
35
+ function styleText(text, ...styles) {
36
+ return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
37
+ }
38
+
27
39
  const logLevelOption = {
28
40
  long: 'logLevel',
29
41
  type: 'string',
@@ -190,14 +202,28 @@ const BUILTIN_VERSION_OPTION = {
190
202
  args: 'none',
191
203
  desc: 'Show version number',
192
204
  };
205
+ const BUILTIN_COLOR_OPTION = {
206
+ long: 'color',
207
+ type: 'boolean',
208
+ args: 'none',
209
+ desc: 'Enable colored help output',
210
+ default: true,
211
+ };
212
+ function createBuiltinOptionState(enabled) {
213
+ return {
214
+ color: enabled,
215
+ logLevel: enabled,
216
+ silent: enabled,
217
+ logDate: enabled,
218
+ logColorful: enabled,
219
+ };
220
+ }
221
+ function isNoColorEnabled(envs) {
222
+ return envs['NO_COLOR'] !== undefined;
223
+ }
193
224
  function normalizeBuiltinConfig(builtin) {
194
225
  const resolved = {
195
- option: {
196
- logLevel: true,
197
- silent: true,
198
- logDate: true,
199
- logColorful: true,
200
- },
226
+ option: createBuiltinOptionState(true),
201
227
  command: {
202
228
  help: false,
203
229
  },
@@ -207,26 +233,29 @@ function normalizeBuiltinConfig(builtin) {
207
233
  }
208
234
  if (builtin === true) {
209
235
  return {
210
- option: { logLevel: true, silent: true, logDate: true, logColorful: true },
236
+ option: createBuiltinOptionState(true),
211
237
  command: { help: true },
212
238
  };
213
239
  }
214
240
  if (builtin === false) {
215
241
  return {
216
- option: { logLevel: false, silent: false, logDate: false, logColorful: false },
242
+ option: createBuiltinOptionState(false),
217
243
  command: { help: false },
218
244
  };
219
245
  }
220
246
  if (builtin.option !== undefined) {
221
247
  if (builtin.option === false) {
222
- resolved.option = { logLevel: false, silent: false, logDate: false, logColorful: false };
248
+ resolved.option = createBuiltinOptionState(false);
223
249
  }
224
250
  else if (builtin.option === true) {
225
- resolved.option = { logLevel: true, silent: true, logDate: true, logColorful: true };
251
+ resolved.option = createBuiltinOptionState(true);
226
252
  }
227
253
  else {
228
- if (builtin.option.logLevel !== undefined)
254
+ if (builtin.option.color !== undefined)
255
+ resolved.option.color = builtin.option.color;
256
+ if (builtin.option.logLevel !== undefined) {
229
257
  resolved.option.logLevel = builtin.option.logLevel;
258
+ }
230
259
  if (builtin.option.silent !== undefined)
231
260
  resolved.option.silent = builtin.option.silent;
232
261
  if (builtin.option.logDate !== undefined)
@@ -258,6 +287,7 @@ class Command {
258
287
  #parent;
259
288
  #options = [];
260
289
  #arguments = [];
290
+ #examples = [];
261
291
  #subcommandsList = [];
262
292
  #subcommandsMap = new Map();
263
293
  #action = undefined;
@@ -286,6 +316,9 @@ class Command {
286
316
  get arguments() {
287
317
  return [...this.#arguments];
288
318
  }
319
+ get examples() {
320
+ return this.#examples.map(example => ({ ...example }));
321
+ }
289
322
  get subcommands() {
290
323
  return new Map(this.#subcommandsMap);
291
324
  }
@@ -304,6 +337,10 @@ class Command {
304
337
  this.#action = fn;
305
338
  return this;
306
339
  }
340
+ example(title, usage, desc) {
341
+ this.#examples.push(this.#normalizeExample({ title, usage, desc }));
342
+ return this;
343
+ }
307
344
  subcommand(name, cmd) {
308
345
  if (this.#builtin.command.help && name === 'help') {
309
346
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
@@ -337,7 +374,8 @@ class Command {
337
374
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
338
375
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
339
376
  if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
340
- console.log(leafCommand.formatHelp());
377
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
378
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
341
379
  return;
342
380
  }
343
381
  if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
@@ -364,7 +402,8 @@ class Command {
364
402
  await leafCommand.#runAction(actionParams);
365
403
  }
366
404
  else if (leafCommand.#subcommandsList.length > 0) {
367
- console.log(leafCommand.formatHelp());
405
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
406
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
368
407
  }
369
408
  else {
370
409
  throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
@@ -397,10 +436,21 @@ class Command {
397
436
  return this.#parse(chain, resolveResult, ctx, restArgs);
398
437
  }
399
438
  formatHelp() {
400
- const lines = [];
439
+ return this.#renderHelpPlain(this.#buildHelpData());
440
+ }
441
+ #formatHelpForDisplay(params = {}) {
442
+ const { color = true } = params;
443
+ const helpData = this.#buildHelpData();
444
+ if (!this.#shouldRenderStyledHelp(color)) {
445
+ return this.#renderHelpPlain(helpData);
446
+ }
447
+ return this.#renderHelpTerminal(helpData);
448
+ }
449
+ #shouldRenderStyledHelp(color) {
450
+ return color && process.stdout.isTTY === true;
451
+ }
452
+ #buildHelpData() {
401
453
  const allOptions = this.#getMergedOptions();
402
- lines.push(this.#desc);
403
- lines.push('');
404
454
  const commandPath = this.#getCommandPath();
405
455
  let usage = `Usage: ${commandPath}`;
406
456
  if (allOptions.length > 0)
@@ -418,61 +468,122 @@ class Command {
418
468
  usage += ` [${arg.name}...]`;
419
469
  }
420
470
  }
421
- lines.push(usage);
471
+ const options = [];
472
+ for (const opt of allOptions) {
473
+ const kebabLong = camelToKebabCase$1(opt.long);
474
+ let sig = opt.short ? `-${opt.short}, ` : ' ';
475
+ sig += `--${kebabLong}`;
476
+ if (opt.args !== 'none') {
477
+ sig += ' <value>';
478
+ }
479
+ let desc = opt.desc;
480
+ if (opt.default !== undefined && opt.type !== 'boolean') {
481
+ desc += ` (default: ${JSON.stringify(opt.default)})`;
482
+ }
483
+ if (opt.choices) {
484
+ desc += ` [choices: ${opt.choices.join(', ')}]`;
485
+ }
486
+ options.push({ sig, desc });
487
+ if (opt.type === 'boolean' && opt.args === 'none') {
488
+ options.push({
489
+ sig: ` --no-${kebabLong}`,
490
+ desc: `Negate --${kebabLong}`,
491
+ });
492
+ }
493
+ }
494
+ const commands = [];
495
+ const showHelpSubcommand = this.#builtin.command.help && this.#subcommandsList.length > 0;
496
+ if (showHelpSubcommand) {
497
+ commands.push({ name: 'help', desc: 'Show help for a command' });
498
+ }
499
+ for (const entry of this.#subcommandsList) {
500
+ let name = entry.name;
501
+ if (entry.aliases.length > 0) {
502
+ name += `, ${entry.aliases.join(', ')}`;
503
+ }
504
+ commands.push({ name, desc: entry.command.#desc });
505
+ }
506
+ const examples = this.#examples.map(example => ({
507
+ title: example.title,
508
+ usage: commandPath ? `${commandPath} ${example.usage}` : example.usage,
509
+ desc: example.desc,
510
+ }));
511
+ return {
512
+ desc: this.#desc,
513
+ usage,
514
+ options,
515
+ commands,
516
+ examples,
517
+ };
518
+ }
519
+ #renderHelpPlain(helpData) {
520
+ const lines = [];
521
+ lines.push(helpData.desc);
422
522
  lines.push('');
423
- if (allOptions.length > 0) {
523
+ lines.push(helpData.usage);
524
+ lines.push('');
525
+ if (helpData.options.length > 0) {
424
526
  lines.push('Options:');
425
- const optLines = [];
426
- for (const opt of allOptions) {
427
- const kebabLong = camelToKebabCase$1(opt.long);
428
- let sig = opt.short ? `-${opt.short}, ` : ' ';
429
- sig += `--${kebabLong}`;
430
- if (opt.args !== 'none') {
431
- sig += ' <value>';
432
- }
433
- let desc = opt.desc;
434
- if (opt.default !== undefined && opt.type !== 'boolean') {
435
- desc += ` (default: ${JSON.stringify(opt.default)})`;
436
- }
437
- if (opt.choices) {
438
- desc += ` [choices: ${opt.choices.join(', ')}]`;
439
- }
440
- optLines.push({ sig, desc });
441
- if (opt.type === 'boolean' && opt.args === 'none') {
442
- optLines.push({
443
- sig: ` --no-${kebabLong}`,
444
- desc: `Negate --${kebabLong}`,
445
- });
446
- }
447
- }
448
- const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
449
- for (const { sig, desc } of optLines) {
527
+ const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
528
+ for (const { sig, desc } of helpData.options) {
450
529
  const padding = ' '.repeat(maxSigLen - sig.length + 2);
451
530
  lines.push(` ${sig}${padding}${desc}`);
452
531
  }
453
532
  lines.push('');
454
533
  }
455
- const showHelpSubcommand = this.#builtin.command.help && this.#subcommandsList.length > 0;
456
- if (this.#subcommandsList.length > 0) {
534
+ if (helpData.commands.length > 0) {
457
535
  lines.push('Commands:');
458
- const cmdLines = [];
459
- if (showHelpSubcommand) {
460
- cmdLines.push({ name: 'help', desc: 'Show help for a command' });
461
- }
462
- for (const entry of this.#subcommandsList) {
463
- let name = entry.name;
464
- if (entry.aliases.length > 0) {
465
- name += `, ${entry.aliases.join(', ')}`;
466
- }
467
- cmdLines.push({ name, desc: entry.command.#desc });
468
- }
469
- const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
470
- for (const { name, desc } of cmdLines) {
536
+ const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
537
+ for (const { name, desc } of helpData.commands) {
471
538
  const padding = ' '.repeat(maxNameLen - name.length + 2);
472
539
  lines.push(` ${name}${padding}${desc}`);
473
540
  }
474
541
  lines.push('');
475
542
  }
543
+ if (helpData.examples.length > 0) {
544
+ lines.push('Examples:');
545
+ for (const example of helpData.examples) {
546
+ lines.push(` - ${example.title}`);
547
+ lines.push(` ${example.usage}`);
548
+ lines.push(` ${example.desc}`);
549
+ lines.push('');
550
+ }
551
+ }
552
+ return lines.join('\n');
553
+ }
554
+ #renderHelpTerminal(helpData) {
555
+ const lines = [];
556
+ lines.push(helpData.desc);
557
+ lines.push('');
558
+ lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
559
+ lines.push('');
560
+ if (helpData.options.length > 0) {
561
+ lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
562
+ const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
563
+ for (const { sig, desc } of helpData.options) {
564
+ const padding = ' '.repeat(maxSigLen - sig.length + 2);
565
+ lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
566
+ }
567
+ lines.push('');
568
+ }
569
+ if (helpData.commands.length > 0) {
570
+ lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
571
+ const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
572
+ for (const { name, desc } of helpData.commands) {
573
+ const padding = ' '.repeat(maxNameLen - name.length + 2);
574
+ lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
575
+ }
576
+ lines.push('');
577
+ }
578
+ if (helpData.examples.length > 0) {
579
+ lines.push(styleText('Examples:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
580
+ for (const example of helpData.examples) {
581
+ lines.push(` - ${styleText(example.title, TERMINAL_STYLE.bold)}`);
582
+ lines.push(` ${styleText(example.usage, TERMINAL_STYLE.cyan)}`);
583
+ lines.push(` ${styleText(example.desc, TERMINAL_STYLE.italic, TERMINAL_STYLE.dim)}`);
584
+ lines.push('');
585
+ }
586
+ }
476
587
  return lines.join('\n');
477
588
  }
478
589
  getCompletionMeta() {
@@ -636,7 +747,7 @@ class Command {
636
747
  const cmd = chain[i];
637
748
  const includeVersion = i === 0;
638
749
  const tokens = consumedTokens.get(cmd) ?? [];
639
- const opts = cmd.#parseOptions(tokens, includeVersion);
750
+ const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
640
751
  optsMap.set(cmd, opts);
641
752
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
642
753
  if (opt.apply && opts[opt.long] !== undefined) {
@@ -652,9 +763,10 @@ class Command {
652
763
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
653
764
  return { ctx, opts: mergedOpts, args, rawArgs };
654
765
  }
655
- #parseOptions(tokens, includeVersion) {
766
+ #parseOptions(tokens, includeVersion, envs) {
656
767
  const allOptions = this.#getMergedOptions(includeVersion);
657
768
  const opts = {};
769
+ let sawColorToken = false;
658
770
  for (const opt of allOptions) {
659
771
  if (opt.default !== undefined) {
660
772
  opts[opt.long] = opt.default;
@@ -682,6 +794,9 @@ class Command {
682
794
  i += 1;
683
795
  continue;
684
796
  }
797
+ if (opt.long === 'color') {
798
+ sawColorToken = true;
799
+ }
685
800
  const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
686
801
  if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
687
802
  throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
@@ -758,6 +873,9 @@ class Command {
758
873
  }
759
874
  }
760
875
  }
876
+ if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
877
+ opts['color'] = false;
878
+ }
761
879
  return opts;
762
880
  }
763
881
  #convertValue(opt, rawValue) {
@@ -830,12 +948,16 @@ class Command {
830
948
  }
831
949
  #getMergedOptions(includeVersion = !this.#parent) {
832
950
  const optionMap = new Map();
951
+ const hasUserColor = this.#options.some(o => o.long === 'color');
833
952
  const hasUserHelp = this.#options.some(o => o.long === 'help');
834
953
  const hasUserVersion = this.#options.some(o => o.long === 'version');
835
954
  const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
836
955
  const hasUserSilent = this.#options.some(o => o.long === 'silent');
837
956
  const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
838
957
  const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
958
+ if (this.#builtin.option.color && !hasUserColor) {
959
+ optionMap.set('color', BUILTIN_COLOR_OPTION);
960
+ }
839
961
  if (!hasUserHelp) {
840
962
  optionMap.set('help', BUILTIN_HELP_OPTION);
841
963
  }
@@ -929,6 +1051,21 @@ class Command {
929
1051
  }
930
1052
  }
931
1053
  }
1054
+ #normalizeExample(example) {
1055
+ const title = example.title.trim();
1056
+ const usage = example.usage.trim();
1057
+ const desc = example.desc.trim();
1058
+ if (!title) {
1059
+ throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
1060
+ }
1061
+ if (!usage) {
1062
+ throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
1063
+ }
1064
+ if (!desc) {
1065
+ throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
1066
+ }
1067
+ return { title, usage, desc };
1068
+ }
932
1069
  async #runAction(params) {
933
1070
  if (!this.#action)
934
1071
  return;
@@ -945,6 +1082,34 @@ class Command {
945
1082
  process.exit(1);
946
1083
  }
947
1084
  }
1085
+ #resolveHelpColorOption(tokens, envs) {
1086
+ const colorOption = this.#getMergedOptions().find(opt => opt.long === 'color');
1087
+ let color = !isNoColorEnabled(envs);
1088
+ if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
1089
+ return color;
1090
+ }
1091
+ for (const token of tokens) {
1092
+ if (token.type !== 'long' || token.name !== 'color') {
1093
+ continue;
1094
+ }
1095
+ const eqIdx = token.resolved.indexOf('=');
1096
+ if (eqIdx === -1) {
1097
+ color = true;
1098
+ continue;
1099
+ }
1100
+ const value = token.resolved.slice(eqIdx + 1);
1101
+ if (value === 'true') {
1102
+ color = true;
1103
+ }
1104
+ else if (value === 'false') {
1105
+ color = false;
1106
+ }
1107
+ else {
1108
+ throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
1109
+ }
1110
+ }
1111
+ return color;
1112
+ }
948
1113
  #hasFlag(tokens, longName, shortName) {
949
1114
  for (const token of tokens) {
950
1115
  if (token.type === 'long' && token.name === longName) {
package/lib/esm/index.mjs CHANGED
@@ -2,6 +2,18 @@ import { LOG_LEVELS, resolveLogLevel, Reporter } from '@guanghechen/reporter';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
 
5
+ const TERMINAL_STYLE = {
6
+ bold: '\x1b[1m',
7
+ italic: '\x1b[3m',
8
+ underline: '\x1b[4m',
9
+ cyan: '\x1b[36m',
10
+ dim: '\x1b[2m',
11
+ reset: '\x1b[0m',
12
+ };
13
+ function styleText(text, ...styles) {
14
+ return `${styles.join('')}${text}${TERMINAL_STYLE.reset}`;
15
+ }
16
+
5
17
  const logLevelOption = {
6
18
  long: 'logLevel',
7
19
  type: 'string',
@@ -168,14 +180,28 @@ const BUILTIN_VERSION_OPTION = {
168
180
  args: 'none',
169
181
  desc: 'Show version number',
170
182
  };
183
+ const BUILTIN_COLOR_OPTION = {
184
+ long: 'color',
185
+ type: 'boolean',
186
+ args: 'none',
187
+ desc: 'Enable colored help output',
188
+ default: true,
189
+ };
190
+ function createBuiltinOptionState(enabled) {
191
+ return {
192
+ color: enabled,
193
+ logLevel: enabled,
194
+ silent: enabled,
195
+ logDate: enabled,
196
+ logColorful: enabled,
197
+ };
198
+ }
199
+ function isNoColorEnabled(envs) {
200
+ return envs['NO_COLOR'] !== undefined;
201
+ }
171
202
  function normalizeBuiltinConfig(builtin) {
172
203
  const resolved = {
173
- option: {
174
- logLevel: true,
175
- silent: true,
176
- logDate: true,
177
- logColorful: true,
178
- },
204
+ option: createBuiltinOptionState(true),
179
205
  command: {
180
206
  help: false,
181
207
  },
@@ -185,26 +211,29 @@ function normalizeBuiltinConfig(builtin) {
185
211
  }
186
212
  if (builtin === true) {
187
213
  return {
188
- option: { logLevel: true, silent: true, logDate: true, logColorful: true },
214
+ option: createBuiltinOptionState(true),
189
215
  command: { help: true },
190
216
  };
191
217
  }
192
218
  if (builtin === false) {
193
219
  return {
194
- option: { logLevel: false, silent: false, logDate: false, logColorful: false },
220
+ option: createBuiltinOptionState(false),
195
221
  command: { help: false },
196
222
  };
197
223
  }
198
224
  if (builtin.option !== undefined) {
199
225
  if (builtin.option === false) {
200
- resolved.option = { logLevel: false, silent: false, logDate: false, logColorful: false };
226
+ resolved.option = createBuiltinOptionState(false);
201
227
  }
202
228
  else if (builtin.option === true) {
203
- resolved.option = { logLevel: true, silent: true, logDate: true, logColorful: true };
229
+ resolved.option = createBuiltinOptionState(true);
204
230
  }
205
231
  else {
206
- if (builtin.option.logLevel !== undefined)
232
+ if (builtin.option.color !== undefined)
233
+ resolved.option.color = builtin.option.color;
234
+ if (builtin.option.logLevel !== undefined) {
207
235
  resolved.option.logLevel = builtin.option.logLevel;
236
+ }
208
237
  if (builtin.option.silent !== undefined)
209
238
  resolved.option.silent = builtin.option.silent;
210
239
  if (builtin.option.logDate !== undefined)
@@ -236,6 +265,7 @@ class Command {
236
265
  #parent;
237
266
  #options = [];
238
267
  #arguments = [];
268
+ #examples = [];
239
269
  #subcommandsList = [];
240
270
  #subcommandsMap = new Map();
241
271
  #action = undefined;
@@ -264,6 +294,9 @@ class Command {
264
294
  get arguments() {
265
295
  return [...this.#arguments];
266
296
  }
297
+ get examples() {
298
+ return this.#examples.map(example => ({ ...example }));
299
+ }
267
300
  get subcommands() {
268
301
  return new Map(this.#subcommandsMap);
269
302
  }
@@ -282,6 +315,10 @@ class Command {
282
315
  this.#action = fn;
283
316
  return this;
284
317
  }
318
+ example(title, usage, desc) {
319
+ this.#examples.push(this.#normalizeExample({ title, usage, desc }));
320
+ return this;
321
+ }
285
322
  subcommand(name, cmd) {
286
323
  if (this.#builtin.command.help && name === 'help') {
287
324
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
@@ -315,7 +352,8 @@ class Command {
315
352
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
316
353
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
317
354
  if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
318
- console.log(leafCommand.formatHelp());
355
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
356
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
319
357
  return;
320
358
  }
321
359
  if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
@@ -342,7 +380,8 @@ class Command {
342
380
  await leafCommand.#runAction(actionParams);
343
381
  }
344
382
  else if (leafCommand.#subcommandsList.length > 0) {
345
- console.log(leafCommand.formatHelp());
383
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
384
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
346
385
  }
347
386
  else {
348
387
  throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
@@ -375,10 +414,21 @@ class Command {
375
414
  return this.#parse(chain, resolveResult, ctx, restArgs);
376
415
  }
377
416
  formatHelp() {
378
- const lines = [];
417
+ return this.#renderHelpPlain(this.#buildHelpData());
418
+ }
419
+ #formatHelpForDisplay(params = {}) {
420
+ const { color = true } = params;
421
+ const helpData = this.#buildHelpData();
422
+ if (!this.#shouldRenderStyledHelp(color)) {
423
+ return this.#renderHelpPlain(helpData);
424
+ }
425
+ return this.#renderHelpTerminal(helpData);
426
+ }
427
+ #shouldRenderStyledHelp(color) {
428
+ return color && process.stdout.isTTY === true;
429
+ }
430
+ #buildHelpData() {
379
431
  const allOptions = this.#getMergedOptions();
380
- lines.push(this.#desc);
381
- lines.push('');
382
432
  const commandPath = this.#getCommandPath();
383
433
  let usage = `Usage: ${commandPath}`;
384
434
  if (allOptions.length > 0)
@@ -396,61 +446,122 @@ class Command {
396
446
  usage += ` [${arg.name}...]`;
397
447
  }
398
448
  }
399
- lines.push(usage);
449
+ const options = [];
450
+ for (const opt of allOptions) {
451
+ const kebabLong = camelToKebabCase$1(opt.long);
452
+ let sig = opt.short ? `-${opt.short}, ` : ' ';
453
+ sig += `--${kebabLong}`;
454
+ if (opt.args !== 'none') {
455
+ sig += ' <value>';
456
+ }
457
+ let desc = opt.desc;
458
+ if (opt.default !== undefined && opt.type !== 'boolean') {
459
+ desc += ` (default: ${JSON.stringify(opt.default)})`;
460
+ }
461
+ if (opt.choices) {
462
+ desc += ` [choices: ${opt.choices.join(', ')}]`;
463
+ }
464
+ options.push({ sig, desc });
465
+ if (opt.type === 'boolean' && opt.args === 'none') {
466
+ options.push({
467
+ sig: ` --no-${kebabLong}`,
468
+ desc: `Negate --${kebabLong}`,
469
+ });
470
+ }
471
+ }
472
+ const commands = [];
473
+ const showHelpSubcommand = this.#builtin.command.help && this.#subcommandsList.length > 0;
474
+ if (showHelpSubcommand) {
475
+ commands.push({ name: 'help', desc: 'Show help for a command' });
476
+ }
477
+ for (const entry of this.#subcommandsList) {
478
+ let name = entry.name;
479
+ if (entry.aliases.length > 0) {
480
+ name += `, ${entry.aliases.join(', ')}`;
481
+ }
482
+ commands.push({ name, desc: entry.command.#desc });
483
+ }
484
+ const examples = this.#examples.map(example => ({
485
+ title: example.title,
486
+ usage: commandPath ? `${commandPath} ${example.usage}` : example.usage,
487
+ desc: example.desc,
488
+ }));
489
+ return {
490
+ desc: this.#desc,
491
+ usage,
492
+ options,
493
+ commands,
494
+ examples,
495
+ };
496
+ }
497
+ #renderHelpPlain(helpData) {
498
+ const lines = [];
499
+ lines.push(helpData.desc);
400
500
  lines.push('');
401
- if (allOptions.length > 0) {
501
+ lines.push(helpData.usage);
502
+ lines.push('');
503
+ if (helpData.options.length > 0) {
402
504
  lines.push('Options:');
403
- const optLines = [];
404
- for (const opt of allOptions) {
405
- const kebabLong = camelToKebabCase$1(opt.long);
406
- let sig = opt.short ? `-${opt.short}, ` : ' ';
407
- sig += `--${kebabLong}`;
408
- if (opt.args !== 'none') {
409
- sig += ' <value>';
410
- }
411
- let desc = opt.desc;
412
- if (opt.default !== undefined && opt.type !== 'boolean') {
413
- desc += ` (default: ${JSON.stringify(opt.default)})`;
414
- }
415
- if (opt.choices) {
416
- desc += ` [choices: ${opt.choices.join(', ')}]`;
417
- }
418
- optLines.push({ sig, desc });
419
- if (opt.type === 'boolean' && opt.args === 'none') {
420
- optLines.push({
421
- sig: ` --no-${kebabLong}`,
422
- desc: `Negate --${kebabLong}`,
423
- });
424
- }
425
- }
426
- const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
427
- for (const { sig, desc } of optLines) {
505
+ const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
506
+ for (const { sig, desc } of helpData.options) {
428
507
  const padding = ' '.repeat(maxSigLen - sig.length + 2);
429
508
  lines.push(` ${sig}${padding}${desc}`);
430
509
  }
431
510
  lines.push('');
432
511
  }
433
- const showHelpSubcommand = this.#builtin.command.help && this.#subcommandsList.length > 0;
434
- if (this.#subcommandsList.length > 0) {
512
+ if (helpData.commands.length > 0) {
435
513
  lines.push('Commands:');
436
- const cmdLines = [];
437
- if (showHelpSubcommand) {
438
- cmdLines.push({ name: 'help', desc: 'Show help for a command' });
439
- }
440
- for (const entry of this.#subcommandsList) {
441
- let name = entry.name;
442
- if (entry.aliases.length > 0) {
443
- name += `, ${entry.aliases.join(', ')}`;
444
- }
445
- cmdLines.push({ name, desc: entry.command.#desc });
446
- }
447
- const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
448
- for (const { name, desc } of cmdLines) {
514
+ const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
515
+ for (const { name, desc } of helpData.commands) {
449
516
  const padding = ' '.repeat(maxNameLen - name.length + 2);
450
517
  lines.push(` ${name}${padding}${desc}`);
451
518
  }
452
519
  lines.push('');
453
520
  }
521
+ if (helpData.examples.length > 0) {
522
+ lines.push('Examples:');
523
+ for (const example of helpData.examples) {
524
+ lines.push(` - ${example.title}`);
525
+ lines.push(` ${example.usage}`);
526
+ lines.push(` ${example.desc}`);
527
+ lines.push('');
528
+ }
529
+ }
530
+ return lines.join('\n');
531
+ }
532
+ #renderHelpTerminal(helpData) {
533
+ const lines = [];
534
+ lines.push(helpData.desc);
535
+ lines.push('');
536
+ lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
537
+ lines.push('');
538
+ if (helpData.options.length > 0) {
539
+ lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
540
+ const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
541
+ for (const { sig, desc } of helpData.options) {
542
+ const padding = ' '.repeat(maxSigLen - sig.length + 2);
543
+ lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
544
+ }
545
+ lines.push('');
546
+ }
547
+ if (helpData.commands.length > 0) {
548
+ lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
549
+ const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
550
+ for (const { name, desc } of helpData.commands) {
551
+ const padding = ' '.repeat(maxNameLen - name.length + 2);
552
+ lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
553
+ }
554
+ lines.push('');
555
+ }
556
+ if (helpData.examples.length > 0) {
557
+ lines.push(styleText('Examples:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
558
+ for (const example of helpData.examples) {
559
+ lines.push(` - ${styleText(example.title, TERMINAL_STYLE.bold)}`);
560
+ lines.push(` ${styleText(example.usage, TERMINAL_STYLE.cyan)}`);
561
+ lines.push(` ${styleText(example.desc, TERMINAL_STYLE.italic, TERMINAL_STYLE.dim)}`);
562
+ lines.push('');
563
+ }
564
+ }
454
565
  return lines.join('\n');
455
566
  }
456
567
  getCompletionMeta() {
@@ -614,7 +725,7 @@ class Command {
614
725
  const cmd = chain[i];
615
726
  const includeVersion = i === 0;
616
727
  const tokens = consumedTokens.get(cmd) ?? [];
617
- const opts = cmd.#parseOptions(tokens, includeVersion);
728
+ const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
618
729
  optsMap.set(cmd, opts);
619
730
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
620
731
  if (opt.apply && opts[opt.long] !== undefined) {
@@ -630,9 +741,10 @@ class Command {
630
741
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
631
742
  return { ctx, opts: mergedOpts, args, rawArgs };
632
743
  }
633
- #parseOptions(tokens, includeVersion) {
744
+ #parseOptions(tokens, includeVersion, envs) {
634
745
  const allOptions = this.#getMergedOptions(includeVersion);
635
746
  const opts = {};
747
+ let sawColorToken = false;
636
748
  for (const opt of allOptions) {
637
749
  if (opt.default !== undefined) {
638
750
  opts[opt.long] = opt.default;
@@ -660,6 +772,9 @@ class Command {
660
772
  i += 1;
661
773
  continue;
662
774
  }
775
+ if (opt.long === 'color') {
776
+ sawColorToken = true;
777
+ }
663
778
  const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
664
779
  if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
665
780
  throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
@@ -736,6 +851,9 @@ class Command {
736
851
  }
737
852
  }
738
853
  }
854
+ if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
855
+ opts['color'] = false;
856
+ }
739
857
  return opts;
740
858
  }
741
859
  #convertValue(opt, rawValue) {
@@ -808,12 +926,16 @@ class Command {
808
926
  }
809
927
  #getMergedOptions(includeVersion = !this.#parent) {
810
928
  const optionMap = new Map();
929
+ const hasUserColor = this.#options.some(o => o.long === 'color');
811
930
  const hasUserHelp = this.#options.some(o => o.long === 'help');
812
931
  const hasUserVersion = this.#options.some(o => o.long === 'version');
813
932
  const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
814
933
  const hasUserSilent = this.#options.some(o => o.long === 'silent');
815
934
  const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
816
935
  const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
936
+ if (this.#builtin.option.color && !hasUserColor) {
937
+ optionMap.set('color', BUILTIN_COLOR_OPTION);
938
+ }
817
939
  if (!hasUserHelp) {
818
940
  optionMap.set('help', BUILTIN_HELP_OPTION);
819
941
  }
@@ -907,6 +1029,21 @@ class Command {
907
1029
  }
908
1030
  }
909
1031
  }
1032
+ #normalizeExample(example) {
1033
+ const title = example.title.trim();
1034
+ const usage = example.usage.trim();
1035
+ const desc = example.desc.trim();
1036
+ if (!title) {
1037
+ throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
1038
+ }
1039
+ if (!usage) {
1040
+ throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
1041
+ }
1042
+ if (!desc) {
1043
+ throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
1044
+ }
1045
+ return { title, usage, desc };
1046
+ }
910
1047
  async #runAction(params) {
911
1048
  if (!this.#action)
912
1049
  return;
@@ -923,6 +1060,34 @@ class Command {
923
1060
  process.exit(1);
924
1061
  }
925
1062
  }
1063
+ #resolveHelpColorOption(tokens, envs) {
1064
+ const colorOption = this.#getMergedOptions().find(opt => opt.long === 'color');
1065
+ let color = !isNoColorEnabled(envs);
1066
+ if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
1067
+ return color;
1068
+ }
1069
+ for (const token of tokens) {
1070
+ if (token.type !== 'long' || token.name !== 'color') {
1071
+ continue;
1072
+ }
1073
+ const eqIdx = token.resolved.indexOf('=');
1074
+ if (eqIdx === -1) {
1075
+ color = true;
1076
+ continue;
1077
+ }
1078
+ const value = token.resolved.slice(eqIdx + 1);
1079
+ if (value === 'true') {
1080
+ color = true;
1081
+ }
1082
+ else if (value === 'false') {
1083
+ color = false;
1084
+ }
1085
+ else {
1086
+ throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
1087
+ }
1088
+ }
1089
+ return color;
1090
+ }
926
1091
  #hasFlag(tokens, longName, shortName) {
927
1092
  for (const token of tokens) {
928
1093
  if (token.type === 'long' && token.name === longName) {
@@ -93,6 +93,8 @@ interface ICommandArgumentConfig<T = unknown> {
93
93
  coerce?: (rawValue: string) => T;
94
94
  }
95
95
  interface ICommandBuiltinOptionConfig {
96
+ /** Enable built-in --color/--no-color option for help rendering (defaults respect NO_COLOR) */
97
+ color?: boolean;
96
98
  /** Enable built-in --log-level option */
97
99
  logLevel?: boolean;
98
100
  /** Enable built-in --silent option */
@@ -112,6 +114,15 @@ interface ICommandBuiltinConfig {
112
114
  /** Built-in command configuration */
113
115
  command?: boolean | ICommandBuiltinCommandConfig;
114
116
  }
117
+ /** Command example configuration */
118
+ interface ICommandExample {
119
+ /** Example title */
120
+ title: string;
121
+ /** Usage fragment relative to command path */
122
+ usage: string;
123
+ /** Example description */
124
+ desc: string;
125
+ }
115
126
  /** Command configuration */
116
127
  interface ICommandConfig {
117
128
  /** Command name (only for root command) */
@@ -133,6 +144,7 @@ interface ICommand {
133
144
  readonly parent: ICommand | undefined;
134
145
  readonly options: ICommandOptionConfig[];
135
146
  readonly arguments: ICommandArgumentConfig[];
147
+ readonly examples: ICommandExample[];
136
148
  readonly subcommands: Map<string, ICommand>;
137
149
  }
138
150
  /** Execution context */
@@ -283,10 +295,12 @@ declare class Command implements ICommand {
283
295
  get parent(): Command | undefined;
284
296
  get options(): ICommandOptionConfig[];
285
297
  get arguments(): ICommandArgumentConfig[];
298
+ get examples(): ICommandExample[];
286
299
  get subcommands(): Map<string, ICommand>;
287
300
  option<T>(opt: ICommandOptionConfig<T>): this;
288
301
  argument<T>(arg: ICommandArgumentConfig<T>): this;
289
302
  action(fn: ICommandAction): this;
303
+ example(title: string, usage: string, desc: string): this;
290
304
  subcommand(name: string, cmd: Command): this;
291
305
  run(params: ICommandRunParams): Promise<void>;
292
306
  parse(params: ICommandRunParams): ICommandParseResult;
@@ -423,4 +437,4 @@ declare const logColorfulOption: ICommandOptionConfig<boolean>;
423
437
  declare const silentOption: ICommandOptionConfig<boolean>;
424
438
 
425
439
  export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
426
- export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandConfig, ICommandContext, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
440
+ export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandConfig, ICommandContext, ICommandExample, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",