@guanghechen/commander 4.1.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,23 @@
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
+
9
+ ## 4.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - feat(reporter): add setFlight API for flight tracking feat(commander): unify builtin config for
14
+ options and commands
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies:
19
+ - @guanghechen/reporter@3.3.0
20
+
3
21
  ## 4.1.0
4
22
 
5
23
  ### 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,69 @@ 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
+
39
+ const logLevelOption = {
40
+ long: 'logLevel',
41
+ type: 'string',
42
+ args: 'required',
43
+ desc: 'Set log level',
44
+ default: 'info',
45
+ choices: reporter.LOG_LEVELS,
46
+ coerce: (raw) => {
47
+ const level = reporter.resolveLogLevel(raw);
48
+ if (level === undefined) {
49
+ throw new Error(`Invalid log level: ${raw}`);
50
+ }
51
+ return level;
52
+ },
53
+ apply: (value, ctx) => {
54
+ ctx.reporter.setLevel(value);
55
+ },
56
+ };
57
+ const logDateOption = {
58
+ long: 'logDate',
59
+ type: 'boolean',
60
+ args: 'none',
61
+ desc: 'Enable log timestamp',
62
+ default: true,
63
+ apply: (value, ctx) => {
64
+ ctx.reporter.setFlight({ date: Boolean(value) });
65
+ },
66
+ };
67
+ const logColorfulOption = {
68
+ long: 'logColorful',
69
+ type: 'boolean',
70
+ args: 'none',
71
+ desc: 'Enable colorful log output',
72
+ default: true,
73
+ apply: (value, ctx) => {
74
+ ctx.reporter.setFlight({ color: Boolean(value) });
75
+ },
76
+ };
77
+ const silentOption = {
78
+ long: 'silent',
79
+ type: 'boolean',
80
+ args: 'none',
81
+ desc: 'Suppress non-error output',
82
+ default: false,
83
+ apply: (value, ctx) => {
84
+ if (value) {
85
+ ctx.reporter.setLevel('error');
86
+ }
87
+ },
88
+ };
89
+
27
90
  class CommanderError extends Error {
28
91
  kind;
29
92
  commandPath;
@@ -139,15 +202,92 @@ const BUILTIN_VERSION_OPTION = {
139
202
  args: 'none',
140
203
  desc: 'Show version number',
141
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
+ }
224
+ function normalizeBuiltinConfig(builtin) {
225
+ const resolved = {
226
+ option: createBuiltinOptionState(true),
227
+ command: {
228
+ help: false,
229
+ },
230
+ };
231
+ if (builtin === undefined) {
232
+ return resolved;
233
+ }
234
+ if (builtin === true) {
235
+ return {
236
+ option: createBuiltinOptionState(true),
237
+ command: { help: true },
238
+ };
239
+ }
240
+ if (builtin === false) {
241
+ return {
242
+ option: createBuiltinOptionState(false),
243
+ command: { help: false },
244
+ };
245
+ }
246
+ if (builtin.option !== undefined) {
247
+ if (builtin.option === false) {
248
+ resolved.option = createBuiltinOptionState(false);
249
+ }
250
+ else if (builtin.option === true) {
251
+ resolved.option = createBuiltinOptionState(true);
252
+ }
253
+ else {
254
+ if (builtin.option.color !== undefined)
255
+ resolved.option.color = builtin.option.color;
256
+ if (builtin.option.logLevel !== undefined) {
257
+ resolved.option.logLevel = builtin.option.logLevel;
258
+ }
259
+ if (builtin.option.silent !== undefined)
260
+ resolved.option.silent = builtin.option.silent;
261
+ if (builtin.option.logDate !== undefined)
262
+ resolved.option.logDate = builtin.option.logDate;
263
+ if (builtin.option.logColorful !== undefined) {
264
+ resolved.option.logColorful = builtin.option.logColorful;
265
+ }
266
+ }
267
+ }
268
+ if (builtin.command !== undefined) {
269
+ if (builtin.command === false) {
270
+ resolved.command = { help: false };
271
+ }
272
+ else if (builtin.command === true) {
273
+ resolved.command = { help: true };
274
+ }
275
+ else if (builtin.command.help !== undefined) {
276
+ resolved.command.help = builtin.command.help;
277
+ }
278
+ }
279
+ return resolved;
280
+ }
142
281
  class Command {
143
282
  #name;
144
283
  #desc;
145
284
  #version;
146
- #helpSubcommandEnabled;
285
+ #builtin;
147
286
  #reporter;
148
287
  #parent;
149
288
  #options = [];
150
289
  #arguments = [];
290
+ #examples = [];
151
291
  #subcommandsList = [];
152
292
  #subcommandsMap = new Map();
153
293
  #action = undefined;
@@ -155,7 +295,7 @@ class Command {
155
295
  this.#name = config.name ?? '';
156
296
  this.#desc = config.desc;
157
297
  this.#version = config.version;
158
- this.#helpSubcommandEnabled = config.help ?? false;
298
+ this.#builtin = normalizeBuiltinConfig(config.builtin);
159
299
  this.#reporter = config.reporter;
160
300
  }
161
301
  get name() {
@@ -176,6 +316,9 @@ class Command {
176
316
  get arguments() {
177
317
  return [...this.#arguments];
178
318
  }
319
+ get examples() {
320
+ return this.#examples.map(example => ({ ...example }));
321
+ }
179
322
  get subcommands() {
180
323
  return new Map(this.#subcommandsMap);
181
324
  }
@@ -194,8 +337,12 @@ class Command {
194
337
  this.#action = fn;
195
338
  return this;
196
339
  }
340
+ example(title, usage, desc) {
341
+ this.#examples.push(this.#normalizeExample({ title, usage, desc }));
342
+ return this;
343
+ }
197
344
  subcommand(name, cmd) {
198
- if (this.#helpSubcommandEnabled && name === 'help') {
345
+ if (this.#builtin.command.help && name === 'help') {
199
346
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
200
347
  }
201
348
  if (cmd.#parent && cmd.#parent !== this) {
@@ -227,7 +374,8 @@ class Command {
227
374
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
228
375
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
229
376
  if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
230
- console.log(leafCommand.formatHelp());
377
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
378
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
231
379
  return;
232
380
  }
233
381
  if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
@@ -254,7 +402,8 @@ class Command {
254
402
  await leafCommand.#runAction(actionParams);
255
403
  }
256
404
  else if (leafCommand.#subcommandsList.length > 0) {
257
- console.log(leafCommand.formatHelp());
405
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
406
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
258
407
  }
259
408
  else {
260
409
  throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
@@ -287,10 +436,21 @@ class Command {
287
436
  return this.#parse(chain, resolveResult, ctx, restArgs);
288
437
  }
289
438
  formatHelp() {
290
- 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() {
291
453
  const allOptions = this.#getMergedOptions();
292
- lines.push(this.#desc);
293
- lines.push('');
294
454
  const commandPath = this.#getCommandPath();
295
455
  let usage = `Usage: ${commandPath}`;
296
456
  if (allOptions.length > 0)
@@ -308,61 +468,122 @@ class Command {
308
468
  usage += ` [${arg.name}...]`;
309
469
  }
310
470
  }
311
- 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);
312
522
  lines.push('');
313
- if (allOptions.length > 0) {
523
+ lines.push(helpData.usage);
524
+ lines.push('');
525
+ if (helpData.options.length > 0) {
314
526
  lines.push('Options:');
315
- const optLines = [];
316
- for (const opt of allOptions) {
317
- const kebabLong = camelToKebabCase$1(opt.long);
318
- let sig = opt.short ? `-${opt.short}, ` : ' ';
319
- sig += `--${kebabLong}`;
320
- if (opt.args !== 'none') {
321
- sig += ' <value>';
322
- }
323
- let desc = opt.desc;
324
- if (opt.default !== undefined && opt.type !== 'boolean') {
325
- desc += ` (default: ${JSON.stringify(opt.default)})`;
326
- }
327
- if (opt.choices) {
328
- desc += ` [choices: ${opt.choices.join(', ')}]`;
329
- }
330
- optLines.push({ sig, desc });
331
- if (opt.type === 'boolean' && opt.args === 'none') {
332
- optLines.push({
333
- sig: ` --no-${kebabLong}`,
334
- desc: `Negate --${kebabLong}`,
335
- });
336
- }
337
- }
338
- const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
339
- 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) {
340
529
  const padding = ' '.repeat(maxSigLen - sig.length + 2);
341
530
  lines.push(` ${sig}${padding}${desc}`);
342
531
  }
343
532
  lines.push('');
344
533
  }
345
- const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
346
- if (this.#subcommandsList.length > 0) {
534
+ if (helpData.commands.length > 0) {
347
535
  lines.push('Commands:');
348
- const cmdLines = [];
349
- if (showHelpSubcommand) {
350
- cmdLines.push({ name: 'help', desc: 'Show help for a command' });
351
- }
352
- for (const entry of this.#subcommandsList) {
353
- let name = entry.name;
354
- if (entry.aliases.length > 0) {
355
- name += `, ${entry.aliases.join(', ')}`;
356
- }
357
- cmdLines.push({ name, desc: entry.command.#desc });
358
- }
359
- const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
360
- 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) {
361
538
  const padding = ' '.repeat(maxNameLen - name.length + 2);
362
539
  lines.push(` ${name}${padding}${desc}`);
363
540
  }
364
541
  lines.push('');
365
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
+ }
366
587
  return lines.join('\n');
367
588
  }
368
589
  getCompletionMeta() {
@@ -393,7 +614,7 @@ class Command {
393
614
  };
394
615
  }
395
616
  #processHelpSubcommand(argv) {
396
- if (!this.#helpSubcommandEnabled)
617
+ if (!this.#builtin.command.help)
397
618
  return argv;
398
619
  if (argv.length < 1 || argv[0] !== 'help')
399
620
  return argv;
@@ -526,7 +747,7 @@ class Command {
526
747
  const cmd = chain[i];
527
748
  const includeVersion = i === 0;
528
749
  const tokens = consumedTokens.get(cmd) ?? [];
529
- const opts = cmd.#parseOptions(tokens, includeVersion);
750
+ const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
530
751
  optsMap.set(cmd, opts);
531
752
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
532
753
  if (opt.apply && opts[opt.long] !== undefined) {
@@ -542,9 +763,10 @@ class Command {
542
763
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
543
764
  return { ctx, opts: mergedOpts, args, rawArgs };
544
765
  }
545
- #parseOptions(tokens, includeVersion) {
766
+ #parseOptions(tokens, includeVersion, envs) {
546
767
  const allOptions = this.#getMergedOptions(includeVersion);
547
768
  const opts = {};
769
+ let sawColorToken = false;
548
770
  for (const opt of allOptions) {
549
771
  if (opt.default !== undefined) {
550
772
  opts[opt.long] = opt.default;
@@ -572,6 +794,9 @@ class Command {
572
794
  i += 1;
573
795
  continue;
574
796
  }
797
+ if (opt.long === 'color') {
798
+ sawColorToken = true;
799
+ }
575
800
  const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
576
801
  if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
577
802
  throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
@@ -648,6 +873,9 @@ class Command {
648
873
  }
649
874
  }
650
875
  }
876
+ if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
877
+ opts['color'] = false;
878
+ }
651
879
  return opts;
652
880
  }
653
881
  #convertValue(opt, rawValue) {
@@ -720,14 +948,34 @@ class Command {
720
948
  }
721
949
  #getMergedOptions(includeVersion = !this.#parent) {
722
950
  const optionMap = new Map();
951
+ const hasUserColor = this.#options.some(o => o.long === 'color');
723
952
  const hasUserHelp = this.#options.some(o => o.long === 'help');
724
953
  const hasUserVersion = this.#options.some(o => o.long === 'version');
954
+ const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
955
+ const hasUserSilent = this.#options.some(o => o.long === 'silent');
956
+ const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
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
+ }
725
961
  if (!hasUserHelp) {
726
962
  optionMap.set('help', BUILTIN_HELP_OPTION);
727
963
  }
728
964
  if (!hasUserVersion && includeVersion) {
729
965
  optionMap.set('version', BUILTIN_VERSION_OPTION);
730
966
  }
967
+ if (this.#builtin.option.logLevel && !hasUserLogLevel) {
968
+ optionMap.set('logLevel', logLevelOption);
969
+ }
970
+ if (this.#builtin.option.silent && !hasUserSilent) {
971
+ optionMap.set('silent', silentOption);
972
+ }
973
+ if (this.#builtin.option.logDate && !hasUserLogDate) {
974
+ optionMap.set('logDate', logDateOption);
975
+ }
976
+ if (this.#builtin.option.logColorful && !hasUserLogColorful) {
977
+ optionMap.set('logColorful', logColorfulOption);
978
+ }
731
979
  for (const opt of this.#options) {
732
980
  optionMap.set(opt.long, opt);
733
981
  }
@@ -803,6 +1051,21 @@ class Command {
803
1051
  }
804
1052
  }
805
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
+ }
806
1069
  async #runAction(params) {
807
1070
  if (!this.#action)
808
1071
  return;
@@ -819,6 +1082,34 @@ class Command {
819
1082
  process.exit(1);
820
1083
  }
821
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
+ }
822
1113
  #hasFlag(tokens, longName, shortName) {
823
1114
  for (const token of tokens) {
824
1115
  if (token.type === 'long' && token.name === longName) {
@@ -1201,37 +1492,13 @@ class PwshCompletion {
1201
1492
  }
1202
1493
  }
1203
1494
 
1204
- const logLevelOption = {
1205
- long: 'logLevel',
1206
- type: 'string',
1207
- args: 'required',
1208
- desc: 'Set log level',
1209
- default: 'info',
1210
- choices: reporter.LOG_LEVELS,
1211
- coerce: (raw) => {
1212
- const level = reporter.resolveLogLevel(raw);
1213
- if (level === undefined) {
1214
- throw new Error(`Invalid log level: ${raw}`);
1215
- }
1216
- return level;
1217
- },
1218
- apply: (value, ctx) => {
1219
- ctx.reporter.setLevel(value);
1220
- },
1221
- };
1222
- const silentOption = {
1223
- long: 'silent',
1224
- type: 'boolean',
1225
- args: 'none',
1226
- desc: 'Suppress non-error output',
1227
- default: false,
1228
- };
1229
-
1230
1495
  exports.BashCompletion = BashCompletion;
1231
1496
  exports.Command = Command;
1232
1497
  exports.CommanderError = CommanderError;
1233
1498
  exports.CompletionCommand = CompletionCommand;
1234
1499
  exports.FishCompletion = FishCompletion;
1235
1500
  exports.PwshCompletion = PwshCompletion;
1501
+ exports.logColorfulOption = logColorfulOption;
1502
+ exports.logDateOption = logDateOption;
1236
1503
  exports.logLevelOption = logLevelOption;
1237
1504
  exports.silentOption = silentOption;
package/lib/esm/index.mjs CHANGED
@@ -1,7 +1,70 @@
1
- import { Reporter, LOG_LEVELS, resolveLogLevel } from '@guanghechen/reporter';
1
+ 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
+
17
+ const logLevelOption = {
18
+ long: 'logLevel',
19
+ type: 'string',
20
+ args: 'required',
21
+ desc: 'Set log level',
22
+ default: 'info',
23
+ choices: LOG_LEVELS,
24
+ coerce: (raw) => {
25
+ const level = resolveLogLevel(raw);
26
+ if (level === undefined) {
27
+ throw new Error(`Invalid log level: ${raw}`);
28
+ }
29
+ return level;
30
+ },
31
+ apply: (value, ctx) => {
32
+ ctx.reporter.setLevel(value);
33
+ },
34
+ };
35
+ const logDateOption = {
36
+ long: 'logDate',
37
+ type: 'boolean',
38
+ args: 'none',
39
+ desc: 'Enable log timestamp',
40
+ default: true,
41
+ apply: (value, ctx) => {
42
+ ctx.reporter.setFlight({ date: Boolean(value) });
43
+ },
44
+ };
45
+ const logColorfulOption = {
46
+ long: 'logColorful',
47
+ type: 'boolean',
48
+ args: 'none',
49
+ desc: 'Enable colorful log output',
50
+ default: true,
51
+ apply: (value, ctx) => {
52
+ ctx.reporter.setFlight({ color: Boolean(value) });
53
+ },
54
+ };
55
+ const silentOption = {
56
+ long: 'silent',
57
+ type: 'boolean',
58
+ args: 'none',
59
+ desc: 'Suppress non-error output',
60
+ default: false,
61
+ apply: (value, ctx) => {
62
+ if (value) {
63
+ ctx.reporter.setLevel('error');
64
+ }
65
+ },
66
+ };
67
+
5
68
  class CommanderError extends Error {
6
69
  kind;
7
70
  commandPath;
@@ -117,15 +180,92 @@ const BUILTIN_VERSION_OPTION = {
117
180
  args: 'none',
118
181
  desc: 'Show version number',
119
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
+ }
202
+ function normalizeBuiltinConfig(builtin) {
203
+ const resolved = {
204
+ option: createBuiltinOptionState(true),
205
+ command: {
206
+ help: false,
207
+ },
208
+ };
209
+ if (builtin === undefined) {
210
+ return resolved;
211
+ }
212
+ if (builtin === true) {
213
+ return {
214
+ option: createBuiltinOptionState(true),
215
+ command: { help: true },
216
+ };
217
+ }
218
+ if (builtin === false) {
219
+ return {
220
+ option: createBuiltinOptionState(false),
221
+ command: { help: false },
222
+ };
223
+ }
224
+ if (builtin.option !== undefined) {
225
+ if (builtin.option === false) {
226
+ resolved.option = createBuiltinOptionState(false);
227
+ }
228
+ else if (builtin.option === true) {
229
+ resolved.option = createBuiltinOptionState(true);
230
+ }
231
+ else {
232
+ if (builtin.option.color !== undefined)
233
+ resolved.option.color = builtin.option.color;
234
+ if (builtin.option.logLevel !== undefined) {
235
+ resolved.option.logLevel = builtin.option.logLevel;
236
+ }
237
+ if (builtin.option.silent !== undefined)
238
+ resolved.option.silent = builtin.option.silent;
239
+ if (builtin.option.logDate !== undefined)
240
+ resolved.option.logDate = builtin.option.logDate;
241
+ if (builtin.option.logColorful !== undefined) {
242
+ resolved.option.logColorful = builtin.option.logColorful;
243
+ }
244
+ }
245
+ }
246
+ if (builtin.command !== undefined) {
247
+ if (builtin.command === false) {
248
+ resolved.command = { help: false };
249
+ }
250
+ else if (builtin.command === true) {
251
+ resolved.command = { help: true };
252
+ }
253
+ else if (builtin.command.help !== undefined) {
254
+ resolved.command.help = builtin.command.help;
255
+ }
256
+ }
257
+ return resolved;
258
+ }
120
259
  class Command {
121
260
  #name;
122
261
  #desc;
123
262
  #version;
124
- #helpSubcommandEnabled;
263
+ #builtin;
125
264
  #reporter;
126
265
  #parent;
127
266
  #options = [];
128
267
  #arguments = [];
268
+ #examples = [];
129
269
  #subcommandsList = [];
130
270
  #subcommandsMap = new Map();
131
271
  #action = undefined;
@@ -133,7 +273,7 @@ class Command {
133
273
  this.#name = config.name ?? '';
134
274
  this.#desc = config.desc;
135
275
  this.#version = config.version;
136
- this.#helpSubcommandEnabled = config.help ?? false;
276
+ this.#builtin = normalizeBuiltinConfig(config.builtin);
137
277
  this.#reporter = config.reporter;
138
278
  }
139
279
  get name() {
@@ -154,6 +294,9 @@ class Command {
154
294
  get arguments() {
155
295
  return [...this.#arguments];
156
296
  }
297
+ get examples() {
298
+ return this.#examples.map(example => ({ ...example }));
299
+ }
157
300
  get subcommands() {
158
301
  return new Map(this.#subcommandsMap);
159
302
  }
@@ -172,8 +315,12 @@ class Command {
172
315
  this.#action = fn;
173
316
  return this;
174
317
  }
318
+ example(title, usage, desc) {
319
+ this.#examples.push(this.#normalizeExample({ title, usage, desc }));
320
+ return this;
321
+ }
175
322
  subcommand(name, cmd) {
176
- if (this.#helpSubcommandEnabled && name === 'help') {
323
+ if (this.#builtin.command.help && name === 'help') {
177
324
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
178
325
  }
179
326
  if (cmd.#parent && cmd.#parent !== this) {
@@ -205,7 +352,8 @@ class Command {
205
352
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
206
353
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
207
354
  if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
208
- console.log(leafCommand.formatHelp());
355
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
356
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
209
357
  return;
210
358
  }
211
359
  if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
@@ -232,7 +380,8 @@ class Command {
232
380
  await leafCommand.#runAction(actionParams);
233
381
  }
234
382
  else if (leafCommand.#subcommandsList.length > 0) {
235
- console.log(leafCommand.formatHelp());
383
+ const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
384
+ console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
236
385
  }
237
386
  else {
238
387
  throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
@@ -265,10 +414,21 @@ class Command {
265
414
  return this.#parse(chain, resolveResult, ctx, restArgs);
266
415
  }
267
416
  formatHelp() {
268
- 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() {
269
431
  const allOptions = this.#getMergedOptions();
270
- lines.push(this.#desc);
271
- lines.push('');
272
432
  const commandPath = this.#getCommandPath();
273
433
  let usage = `Usage: ${commandPath}`;
274
434
  if (allOptions.length > 0)
@@ -286,61 +446,122 @@ class Command {
286
446
  usage += ` [${arg.name}...]`;
287
447
  }
288
448
  }
289
- 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);
290
500
  lines.push('');
291
- if (allOptions.length > 0) {
501
+ lines.push(helpData.usage);
502
+ lines.push('');
503
+ if (helpData.options.length > 0) {
292
504
  lines.push('Options:');
293
- const optLines = [];
294
- for (const opt of allOptions) {
295
- const kebabLong = camelToKebabCase$1(opt.long);
296
- let sig = opt.short ? `-${opt.short}, ` : ' ';
297
- sig += `--${kebabLong}`;
298
- if (opt.args !== 'none') {
299
- sig += ' <value>';
300
- }
301
- let desc = opt.desc;
302
- if (opt.default !== undefined && opt.type !== 'boolean') {
303
- desc += ` (default: ${JSON.stringify(opt.default)})`;
304
- }
305
- if (opt.choices) {
306
- desc += ` [choices: ${opt.choices.join(', ')}]`;
307
- }
308
- optLines.push({ sig, desc });
309
- if (opt.type === 'boolean' && opt.args === 'none') {
310
- optLines.push({
311
- sig: ` --no-${kebabLong}`,
312
- desc: `Negate --${kebabLong}`,
313
- });
314
- }
315
- }
316
- const maxSigLen = Math.max(...optLines.map(l => l.sig.length));
317
- 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) {
318
507
  const padding = ' '.repeat(maxSigLen - sig.length + 2);
319
508
  lines.push(` ${sig}${padding}${desc}`);
320
509
  }
321
510
  lines.push('');
322
511
  }
323
- const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
324
- if (this.#subcommandsList.length > 0) {
512
+ if (helpData.commands.length > 0) {
325
513
  lines.push('Commands:');
326
- const cmdLines = [];
327
- if (showHelpSubcommand) {
328
- cmdLines.push({ name: 'help', desc: 'Show help for a command' });
329
- }
330
- for (const entry of this.#subcommandsList) {
331
- let name = entry.name;
332
- if (entry.aliases.length > 0) {
333
- name += `, ${entry.aliases.join(', ')}`;
334
- }
335
- cmdLines.push({ name, desc: entry.command.#desc });
336
- }
337
- const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
338
- 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) {
339
516
  const padding = ' '.repeat(maxNameLen - name.length + 2);
340
517
  lines.push(` ${name}${padding}${desc}`);
341
518
  }
342
519
  lines.push('');
343
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
+ }
344
565
  return lines.join('\n');
345
566
  }
346
567
  getCompletionMeta() {
@@ -371,7 +592,7 @@ class Command {
371
592
  };
372
593
  }
373
594
  #processHelpSubcommand(argv) {
374
- if (!this.#helpSubcommandEnabled)
595
+ if (!this.#builtin.command.help)
375
596
  return argv;
376
597
  if (argv.length < 1 || argv[0] !== 'help')
377
598
  return argv;
@@ -504,7 +725,7 @@ class Command {
504
725
  const cmd = chain[i];
505
726
  const includeVersion = i === 0;
506
727
  const tokens = consumedTokens.get(cmd) ?? [];
507
- const opts = cmd.#parseOptions(tokens, includeVersion);
728
+ const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
508
729
  optsMap.set(cmd, opts);
509
730
  for (const opt of cmd.#getMergedOptions(includeVersion)) {
510
731
  if (opt.apply && opts[opt.long] !== undefined) {
@@ -520,9 +741,10 @@ class Command {
520
741
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
521
742
  return { ctx, opts: mergedOpts, args, rawArgs };
522
743
  }
523
- #parseOptions(tokens, includeVersion) {
744
+ #parseOptions(tokens, includeVersion, envs) {
524
745
  const allOptions = this.#getMergedOptions(includeVersion);
525
746
  const opts = {};
747
+ let sawColorToken = false;
526
748
  for (const opt of allOptions) {
527
749
  if (opt.default !== undefined) {
528
750
  opts[opt.long] = opt.default;
@@ -550,6 +772,9 @@ class Command {
550
772
  i += 1;
551
773
  continue;
552
774
  }
775
+ if (opt.long === 'color') {
776
+ sawColorToken = true;
777
+ }
553
778
  const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
554
779
  if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
555
780
  throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
@@ -626,6 +851,9 @@ class Command {
626
851
  }
627
852
  }
628
853
  }
854
+ if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
855
+ opts['color'] = false;
856
+ }
629
857
  return opts;
630
858
  }
631
859
  #convertValue(opt, rawValue) {
@@ -698,14 +926,34 @@ class Command {
698
926
  }
699
927
  #getMergedOptions(includeVersion = !this.#parent) {
700
928
  const optionMap = new Map();
929
+ const hasUserColor = this.#options.some(o => o.long === 'color');
701
930
  const hasUserHelp = this.#options.some(o => o.long === 'help');
702
931
  const hasUserVersion = this.#options.some(o => o.long === 'version');
932
+ const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
933
+ const hasUserSilent = this.#options.some(o => o.long === 'silent');
934
+ const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
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
+ }
703
939
  if (!hasUserHelp) {
704
940
  optionMap.set('help', BUILTIN_HELP_OPTION);
705
941
  }
706
942
  if (!hasUserVersion && includeVersion) {
707
943
  optionMap.set('version', BUILTIN_VERSION_OPTION);
708
944
  }
945
+ if (this.#builtin.option.logLevel && !hasUserLogLevel) {
946
+ optionMap.set('logLevel', logLevelOption);
947
+ }
948
+ if (this.#builtin.option.silent && !hasUserSilent) {
949
+ optionMap.set('silent', silentOption);
950
+ }
951
+ if (this.#builtin.option.logDate && !hasUserLogDate) {
952
+ optionMap.set('logDate', logDateOption);
953
+ }
954
+ if (this.#builtin.option.logColorful && !hasUserLogColorful) {
955
+ optionMap.set('logColorful', logColorfulOption);
956
+ }
709
957
  for (const opt of this.#options) {
710
958
  optionMap.set(opt.long, opt);
711
959
  }
@@ -781,6 +1029,21 @@ class Command {
781
1029
  }
782
1030
  }
783
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
+ }
784
1047
  async #runAction(params) {
785
1048
  if (!this.#action)
786
1049
  return;
@@ -797,6 +1060,34 @@ class Command {
797
1060
  process.exit(1);
798
1061
  }
799
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
+ }
800
1091
  #hasFlag(tokens, longName, shortName) {
801
1092
  for (const token of tokens) {
802
1093
  if (token.type === 'long' && token.name === longName) {
@@ -1179,30 +1470,4 @@ class PwshCompletion {
1179
1470
  }
1180
1471
  }
1181
1472
 
1182
- const logLevelOption = {
1183
- long: 'logLevel',
1184
- type: 'string',
1185
- args: 'required',
1186
- desc: 'Set log level',
1187
- default: 'info',
1188
- choices: LOG_LEVELS,
1189
- coerce: (raw) => {
1190
- const level = resolveLogLevel(raw);
1191
- if (level === undefined) {
1192
- throw new Error(`Invalid log level: ${raw}`);
1193
- }
1194
- return level;
1195
- },
1196
- apply: (value, ctx) => {
1197
- ctx.reporter.setLevel(value);
1198
- },
1199
- };
1200
- const silentOption = {
1201
- long: 'silent',
1202
- type: 'boolean',
1203
- args: 'none',
1204
- desc: 'Suppress non-error output',
1205
- default: false,
1206
- };
1207
-
1208
- export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logLevelOption, silentOption };
1473
+ export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
@@ -92,6 +92,37 @@ interface ICommandArgumentConfig<T = unknown> {
92
92
  /** Custom value transformation (takes precedence over type conversion) */
93
93
  coerce?: (rawValue: string) => T;
94
94
  }
95
+ interface ICommandBuiltinOptionConfig {
96
+ /** Enable built-in --color/--no-color option for help rendering (defaults respect NO_COLOR) */
97
+ color?: boolean;
98
+ /** Enable built-in --log-level option */
99
+ logLevel?: boolean;
100
+ /** Enable built-in --silent option */
101
+ silent?: boolean;
102
+ /** Enable built-in --log-date/--no-log-date option */
103
+ logDate?: boolean;
104
+ /** Enable built-in --log-colorful/--no-log-colorful option */
105
+ logColorful?: boolean;
106
+ }
107
+ interface ICommandBuiltinCommandConfig {
108
+ /** Enable built-in help subcommand */
109
+ help?: boolean;
110
+ }
111
+ interface ICommandBuiltinConfig {
112
+ /** Built-in options configuration */
113
+ option?: boolean | ICommandBuiltinOptionConfig;
114
+ /** Built-in command configuration */
115
+ command?: boolean | ICommandBuiltinCommandConfig;
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
+ }
95
126
  /** Command configuration */
96
127
  interface ICommandConfig {
97
128
  /** Command name (only for root command) */
@@ -100,8 +131,8 @@ interface ICommandConfig {
100
131
  desc: string;
101
132
  /** Version (for root --version) */
102
133
  version?: string;
103
- /** Enable built-in "help" subcommand */
104
- help?: boolean;
134
+ /** Built-in features configuration */
135
+ builtin?: boolean | ICommandBuiltinConfig;
105
136
  /** Default reporter for this command */
106
137
  reporter?: IReporter;
107
138
  }
@@ -113,6 +144,7 @@ interface ICommand {
113
144
  readonly parent: ICommand | undefined;
114
145
  readonly options: ICommandOptionConfig[];
115
146
  readonly arguments: ICommandArgumentConfig[];
147
+ readonly examples: ICommandExample[];
116
148
  readonly subcommands: Map<string, ICommand>;
117
149
  }
118
150
  /** Execution context */
@@ -263,10 +295,12 @@ declare class Command implements ICommand {
263
295
  get parent(): Command | undefined;
264
296
  get options(): ICommandOptionConfig[];
265
297
  get arguments(): ICommandArgumentConfig[];
298
+ get examples(): ICommandExample[];
266
299
  get subcommands(): Map<string, ICommand>;
267
300
  option<T>(opt: ICommandOptionConfig<T>): this;
268
301
  argument<T>(arg: ICommandArgumentConfig<T>): this;
269
302
  action(fn: ICommandAction): this;
303
+ example(title: string, usage: string, desc: string): this;
270
304
  subcommand(name: string, cmd: Command): this;
271
305
  run(params: ICommandRunParams): Promise<void>;
272
306
  parse(params: ICommandRunParams): ICommandParseResult;
@@ -355,6 +389,28 @@ declare class PwshCompletion {
355
389
  * ```
356
390
  */
357
391
  declare const logLevelOption: ICommandOptionConfig<string>;
392
+ /**
393
+ * Pre-defined --log-date option for controlling timestamp output.
394
+ *
395
+ * | Property | Value |
396
+ * | --------- | --------- |
397
+ * | long | 'logDate' |
398
+ * | type | 'boolean' |
399
+ * | args | 'none' |
400
+ * | default | true |
401
+ */
402
+ declare const logDateOption: ICommandOptionConfig<boolean>;
403
+ /**
404
+ * Pre-defined --log-colorful option for controlling colorful output.
405
+ *
406
+ * | Property | Value |
407
+ * | --------- | ------------- |
408
+ * | long | 'logColorful' |
409
+ * | type | 'boolean' |
410
+ * | args | 'none' |
411
+ * | default | true |
412
+ */
413
+ declare const logColorfulOption: ICommandOptionConfig<boolean>;
358
414
  /**
359
415
  * Pre-defined --silent option for suppressing non-error output.
360
416
  *
@@ -380,5 +436,5 @@ declare const logLevelOption: ICommandOptionConfig<string>;
380
436
  */
381
437
  declare const silentOption: ICommandOptionConfig<boolean>;
382
438
 
383
- export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logLevelOption, silentOption };
384
- export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandConfig, ICommandContext, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
439
+ export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
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.1.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",
@@ -41,7 +41,7 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "@guanghechen/reporter": "^3.2.0"
44
+ "@guanghechen/reporter": "^3.3.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "rollup -c ../../rollup.config.mjs",