@guanghechen/commander 4.2.0 → 4.4.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 +12 -0
- package/README.md +23 -0
- package/lib/cjs/index.cjs +267 -72
- package/lib/esm/index.mjs +267 -72
- package/lib/types/index.d.ts +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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:
|
|
236
|
+
option: createBuiltinOptionState(true),
|
|
211
237
|
command: { help: true },
|
|
212
238
|
};
|
|
213
239
|
}
|
|
214
240
|
if (builtin === false) {
|
|
215
241
|
return {
|
|
216
|
-
option:
|
|
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 =
|
|
248
|
+
resolved.option = createBuiltinOptionState(false);
|
|
223
249
|
}
|
|
224
250
|
else if (builtin.option === true) {
|
|
225
|
-
resolved.option =
|
|
251
|
+
resolved.option = createBuiltinOptionState(true);
|
|
226
252
|
}
|
|
227
253
|
else {
|
|
228
|
-
if (builtin.option.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
522
|
+
lines.push('');
|
|
523
|
+
lines.push(helpData.usage);
|
|
422
524
|
lines.push('');
|
|
423
|
-
if (
|
|
525
|
+
if (helpData.options.length > 0) {
|
|
424
526
|
lines.push('Options:');
|
|
425
|
-
const
|
|
426
|
-
for (const
|
|
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
|
-
|
|
456
|
-
if (this.#subcommandsList.length > 0) {
|
|
534
|
+
if (helpData.commands.length > 0) {
|
|
457
535
|
lines.push('Commands:');
|
|
458
|
-
const
|
|
459
|
-
|
|
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() {
|
|
@@ -502,18 +613,48 @@ class Command {
|
|
|
502
613
|
}),
|
|
503
614
|
};
|
|
504
615
|
}
|
|
616
|
+
#findSubcommandEntry(token) {
|
|
617
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
618
|
+
}
|
|
619
|
+
#createUnknownSubcommandError(subcommand) {
|
|
620
|
+
const commandPath = this.#getCommandPath();
|
|
621
|
+
return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
|
|
622
|
+
}
|
|
505
623
|
#processHelpSubcommand(argv) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
624
|
+
let current = this;
|
|
625
|
+
for (let i = 0; i < argv.length; ++i) {
|
|
626
|
+
const token = argv[i];
|
|
627
|
+
if (token.startsWith('-')) {
|
|
628
|
+
return argv;
|
|
629
|
+
}
|
|
630
|
+
if (token === 'help') {
|
|
631
|
+
if (!current.#builtin.command.help) {
|
|
632
|
+
if (current.#subcommandsList.length > 0) {
|
|
633
|
+
throw current.#createUnknownSubcommandError('help');
|
|
634
|
+
}
|
|
635
|
+
return argv;
|
|
636
|
+
}
|
|
637
|
+
if (current.#subcommandsList.length === 0) {
|
|
638
|
+
return argv;
|
|
639
|
+
}
|
|
640
|
+
const target = argv[i + 1];
|
|
641
|
+
if (target === undefined) {
|
|
642
|
+
return [...argv.slice(0, i), '--help'];
|
|
643
|
+
}
|
|
644
|
+
const targetEntry = current.#findSubcommandEntry(target);
|
|
645
|
+
if (targetEntry === undefined) {
|
|
646
|
+
throw current.#createUnknownSubcommandError(target);
|
|
647
|
+
}
|
|
648
|
+
if (argv[i + 2] !== undefined) {
|
|
649
|
+
throw new CommanderError('UnexpectedArgument', 'help subcommand accepts at most one subcommand argument', current.#getCommandPath());
|
|
650
|
+
}
|
|
651
|
+
return [...argv.slice(0, i), target, '--help'];
|
|
652
|
+
}
|
|
653
|
+
const entry = current.#findSubcommandEntry(token);
|
|
654
|
+
if (entry === undefined) {
|
|
655
|
+
return argv;
|
|
656
|
+
}
|
|
657
|
+
current = entry.command;
|
|
517
658
|
}
|
|
518
659
|
return argv;
|
|
519
660
|
}
|
|
@@ -525,7 +666,7 @@ class Command {
|
|
|
525
666
|
const token = argv[idx];
|
|
526
667
|
if (token.startsWith('-'))
|
|
527
668
|
break;
|
|
528
|
-
const entry = current.#
|
|
669
|
+
const entry = current.#findSubcommandEntry(token);
|
|
529
670
|
if (!entry)
|
|
530
671
|
break;
|
|
531
672
|
current = entry.command;
|
|
@@ -636,7 +777,7 @@ class Command {
|
|
|
636
777
|
const cmd = chain[i];
|
|
637
778
|
const includeVersion = i === 0;
|
|
638
779
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
639
|
-
const opts = cmd.#parseOptions(tokens, includeVersion);
|
|
780
|
+
const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
|
|
640
781
|
optsMap.set(cmd, opts);
|
|
641
782
|
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
642
783
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
@@ -652,9 +793,10 @@ class Command {
|
|
|
652
793
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
653
794
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
654
795
|
}
|
|
655
|
-
#parseOptions(tokens, includeVersion) {
|
|
796
|
+
#parseOptions(tokens, includeVersion, envs) {
|
|
656
797
|
const allOptions = this.#getMergedOptions(includeVersion);
|
|
657
798
|
const opts = {};
|
|
799
|
+
let sawColorToken = false;
|
|
658
800
|
for (const opt of allOptions) {
|
|
659
801
|
if (opt.default !== undefined) {
|
|
660
802
|
opts[opt.long] = opt.default;
|
|
@@ -682,6 +824,9 @@ class Command {
|
|
|
682
824
|
i += 1;
|
|
683
825
|
continue;
|
|
684
826
|
}
|
|
827
|
+
if (opt.long === 'color') {
|
|
828
|
+
sawColorToken = true;
|
|
829
|
+
}
|
|
685
830
|
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
686
831
|
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
687
832
|
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
@@ -758,6 +903,9 @@ class Command {
|
|
|
758
903
|
}
|
|
759
904
|
}
|
|
760
905
|
}
|
|
906
|
+
if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
|
|
907
|
+
opts['color'] = false;
|
|
908
|
+
}
|
|
761
909
|
return opts;
|
|
762
910
|
}
|
|
763
911
|
#convertValue(opt, rawValue) {
|
|
@@ -830,12 +978,16 @@ class Command {
|
|
|
830
978
|
}
|
|
831
979
|
#getMergedOptions(includeVersion = !this.#parent) {
|
|
832
980
|
const optionMap = new Map();
|
|
981
|
+
const hasUserColor = this.#options.some(o => o.long === 'color');
|
|
833
982
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
834
983
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
835
984
|
const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
|
|
836
985
|
const hasUserSilent = this.#options.some(o => o.long === 'silent');
|
|
837
986
|
const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
|
|
838
987
|
const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
|
|
988
|
+
if (this.#builtin.option.color && !hasUserColor) {
|
|
989
|
+
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
990
|
+
}
|
|
839
991
|
if (!hasUserHelp) {
|
|
840
992
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
841
993
|
}
|
|
@@ -929,6 +1081,21 @@ class Command {
|
|
|
929
1081
|
}
|
|
930
1082
|
}
|
|
931
1083
|
}
|
|
1084
|
+
#normalizeExample(example) {
|
|
1085
|
+
const title = example.title.trim();
|
|
1086
|
+
const usage = example.usage.trim();
|
|
1087
|
+
const desc = example.desc.trim();
|
|
1088
|
+
if (!title) {
|
|
1089
|
+
throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
|
|
1090
|
+
}
|
|
1091
|
+
if (!usage) {
|
|
1092
|
+
throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
|
|
1093
|
+
}
|
|
1094
|
+
if (!desc) {
|
|
1095
|
+
throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
|
|
1096
|
+
}
|
|
1097
|
+
return { title, usage, desc };
|
|
1098
|
+
}
|
|
932
1099
|
async #runAction(params) {
|
|
933
1100
|
if (!this.#action)
|
|
934
1101
|
return;
|
|
@@ -945,6 +1112,34 @@ class Command {
|
|
|
945
1112
|
process.exit(1);
|
|
946
1113
|
}
|
|
947
1114
|
}
|
|
1115
|
+
#resolveHelpColorOption(tokens, envs) {
|
|
1116
|
+
const colorOption = this.#getMergedOptions().find(opt => opt.long === 'color');
|
|
1117
|
+
let color = !isNoColorEnabled(envs);
|
|
1118
|
+
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1119
|
+
return color;
|
|
1120
|
+
}
|
|
1121
|
+
for (const token of tokens) {
|
|
1122
|
+
if (token.type !== 'long' || token.name !== 'color') {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1126
|
+
if (eqIdx === -1) {
|
|
1127
|
+
color = true;
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const value = token.resolved.slice(eqIdx + 1);
|
|
1131
|
+
if (value === 'true') {
|
|
1132
|
+
color = true;
|
|
1133
|
+
}
|
|
1134
|
+
else if (value === 'false') {
|
|
1135
|
+
color = false;
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return color;
|
|
1142
|
+
}
|
|
948
1143
|
#hasFlag(tokens, longName, shortName) {
|
|
949
1144
|
for (const token of tokens) {
|
|
950
1145
|
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:
|
|
214
|
+
option: createBuiltinOptionState(true),
|
|
189
215
|
command: { help: true },
|
|
190
216
|
};
|
|
191
217
|
}
|
|
192
218
|
if (builtin === false) {
|
|
193
219
|
return {
|
|
194
|
-
option:
|
|
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 =
|
|
226
|
+
resolved.option = createBuiltinOptionState(false);
|
|
201
227
|
}
|
|
202
228
|
else if (builtin.option === true) {
|
|
203
|
-
resolved.option =
|
|
229
|
+
resolved.option = createBuiltinOptionState(true);
|
|
204
230
|
}
|
|
205
231
|
else {
|
|
206
|
-
if (builtin.option.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
+
lines.push(helpData.usage);
|
|
502
|
+
lines.push('');
|
|
503
|
+
if (helpData.options.length > 0) {
|
|
402
504
|
lines.push('Options:');
|
|
403
|
-
const
|
|
404
|
-
for (const
|
|
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
|
-
|
|
434
|
-
if (this.#subcommandsList.length > 0) {
|
|
512
|
+
if (helpData.commands.length > 0) {
|
|
435
513
|
lines.push('Commands:');
|
|
436
|
-
const
|
|
437
|
-
|
|
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() {
|
|
@@ -480,18 +591,48 @@ class Command {
|
|
|
480
591
|
}),
|
|
481
592
|
};
|
|
482
593
|
}
|
|
594
|
+
#findSubcommandEntry(token) {
|
|
595
|
+
return this.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
596
|
+
}
|
|
597
|
+
#createUnknownSubcommandError(subcommand) {
|
|
598
|
+
const commandPath = this.#getCommandPath();
|
|
599
|
+
return new CommanderError('UnknownSubcommand', `unknown subcommand "${subcommand}" for command "${commandPath}"`, commandPath);
|
|
600
|
+
}
|
|
483
601
|
#processHelpSubcommand(argv) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
602
|
+
let current = this;
|
|
603
|
+
for (let i = 0; i < argv.length; ++i) {
|
|
604
|
+
const token = argv[i];
|
|
605
|
+
if (token.startsWith('-')) {
|
|
606
|
+
return argv;
|
|
607
|
+
}
|
|
608
|
+
if (token === 'help') {
|
|
609
|
+
if (!current.#builtin.command.help) {
|
|
610
|
+
if (current.#subcommandsList.length > 0) {
|
|
611
|
+
throw current.#createUnknownSubcommandError('help');
|
|
612
|
+
}
|
|
613
|
+
return argv;
|
|
614
|
+
}
|
|
615
|
+
if (current.#subcommandsList.length === 0) {
|
|
616
|
+
return argv;
|
|
617
|
+
}
|
|
618
|
+
const target = argv[i + 1];
|
|
619
|
+
if (target === undefined) {
|
|
620
|
+
return [...argv.slice(0, i), '--help'];
|
|
621
|
+
}
|
|
622
|
+
const targetEntry = current.#findSubcommandEntry(target);
|
|
623
|
+
if (targetEntry === undefined) {
|
|
624
|
+
throw current.#createUnknownSubcommandError(target);
|
|
625
|
+
}
|
|
626
|
+
if (argv[i + 2] !== undefined) {
|
|
627
|
+
throw new CommanderError('UnexpectedArgument', 'help subcommand accepts at most one subcommand argument', current.#getCommandPath());
|
|
628
|
+
}
|
|
629
|
+
return [...argv.slice(0, i), target, '--help'];
|
|
630
|
+
}
|
|
631
|
+
const entry = current.#findSubcommandEntry(token);
|
|
632
|
+
if (entry === undefined) {
|
|
633
|
+
return argv;
|
|
634
|
+
}
|
|
635
|
+
current = entry.command;
|
|
495
636
|
}
|
|
496
637
|
return argv;
|
|
497
638
|
}
|
|
@@ -503,7 +644,7 @@ class Command {
|
|
|
503
644
|
const token = argv[idx];
|
|
504
645
|
if (token.startsWith('-'))
|
|
505
646
|
break;
|
|
506
|
-
const entry = current.#
|
|
647
|
+
const entry = current.#findSubcommandEntry(token);
|
|
507
648
|
if (!entry)
|
|
508
649
|
break;
|
|
509
650
|
current = entry.command;
|
|
@@ -614,7 +755,7 @@ class Command {
|
|
|
614
755
|
const cmd = chain[i];
|
|
615
756
|
const includeVersion = i === 0;
|
|
616
757
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
617
|
-
const opts = cmd.#parseOptions(tokens, includeVersion);
|
|
758
|
+
const opts = cmd.#parseOptions(tokens, includeVersion, ctx.envs);
|
|
618
759
|
optsMap.set(cmd, opts);
|
|
619
760
|
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
620
761
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
@@ -630,9 +771,10 @@ class Command {
|
|
|
630
771
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
631
772
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
632
773
|
}
|
|
633
|
-
#parseOptions(tokens, includeVersion) {
|
|
774
|
+
#parseOptions(tokens, includeVersion, envs) {
|
|
634
775
|
const allOptions = this.#getMergedOptions(includeVersion);
|
|
635
776
|
const opts = {};
|
|
777
|
+
let sawColorToken = false;
|
|
636
778
|
for (const opt of allOptions) {
|
|
637
779
|
if (opt.default !== undefined) {
|
|
638
780
|
opts[opt.long] = opt.default;
|
|
@@ -660,6 +802,9 @@ class Command {
|
|
|
660
802
|
i += 1;
|
|
661
803
|
continue;
|
|
662
804
|
}
|
|
805
|
+
if (opt.long === 'color') {
|
|
806
|
+
sawColorToken = true;
|
|
807
|
+
}
|
|
663
808
|
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
664
809
|
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
665
810
|
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
@@ -736,6 +881,9 @@ class Command {
|
|
|
736
881
|
}
|
|
737
882
|
}
|
|
738
883
|
}
|
|
884
|
+
if (isNoColorEnabled(envs) && !sawColorToken && opts['color'] === true) {
|
|
885
|
+
opts['color'] = false;
|
|
886
|
+
}
|
|
739
887
|
return opts;
|
|
740
888
|
}
|
|
741
889
|
#convertValue(opt, rawValue) {
|
|
@@ -808,12 +956,16 @@ class Command {
|
|
|
808
956
|
}
|
|
809
957
|
#getMergedOptions(includeVersion = !this.#parent) {
|
|
810
958
|
const optionMap = new Map();
|
|
959
|
+
const hasUserColor = this.#options.some(o => o.long === 'color');
|
|
811
960
|
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
812
961
|
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
813
962
|
const hasUserLogLevel = this.#options.some(o => o.long === 'logLevel');
|
|
814
963
|
const hasUserSilent = this.#options.some(o => o.long === 'silent');
|
|
815
964
|
const hasUserLogDate = this.#options.some(o => o.long === 'logDate');
|
|
816
965
|
const hasUserLogColorful = this.#options.some(o => o.long === 'logColorful');
|
|
966
|
+
if (this.#builtin.option.color && !hasUserColor) {
|
|
967
|
+
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
968
|
+
}
|
|
817
969
|
if (!hasUserHelp) {
|
|
818
970
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
819
971
|
}
|
|
@@ -907,6 +1059,21 @@ class Command {
|
|
|
907
1059
|
}
|
|
908
1060
|
}
|
|
909
1061
|
}
|
|
1062
|
+
#normalizeExample(example) {
|
|
1063
|
+
const title = example.title.trim();
|
|
1064
|
+
const usage = example.usage.trim();
|
|
1065
|
+
const desc = example.desc.trim();
|
|
1066
|
+
if (!title) {
|
|
1067
|
+
throw new CommanderError('ConfigurationError', 'example title cannot be empty', this.#getCommandPath());
|
|
1068
|
+
}
|
|
1069
|
+
if (!usage) {
|
|
1070
|
+
throw new CommanderError('ConfigurationError', 'example usage cannot be empty', this.#getCommandPath());
|
|
1071
|
+
}
|
|
1072
|
+
if (!desc) {
|
|
1073
|
+
throw new CommanderError('ConfigurationError', 'example description cannot be empty', this.#getCommandPath());
|
|
1074
|
+
}
|
|
1075
|
+
return { title, usage, desc };
|
|
1076
|
+
}
|
|
910
1077
|
async #runAction(params) {
|
|
911
1078
|
if (!this.#action)
|
|
912
1079
|
return;
|
|
@@ -923,6 +1090,34 @@ class Command {
|
|
|
923
1090
|
process.exit(1);
|
|
924
1091
|
}
|
|
925
1092
|
}
|
|
1093
|
+
#resolveHelpColorOption(tokens, envs) {
|
|
1094
|
+
const colorOption = this.#getMergedOptions().find(opt => opt.long === 'color');
|
|
1095
|
+
let color = !isNoColorEnabled(envs);
|
|
1096
|
+
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1097
|
+
return color;
|
|
1098
|
+
}
|
|
1099
|
+
for (const token of tokens) {
|
|
1100
|
+
if (token.type !== 'long' || token.name !== 'color') {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
1104
|
+
if (eqIdx === -1) {
|
|
1105
|
+
color = true;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const value = token.resolved.slice(eqIdx + 1);
|
|
1109
|
+
if (value === 'true') {
|
|
1110
|
+
color = true;
|
|
1111
|
+
}
|
|
1112
|
+
else if (value === 'false') {
|
|
1113
|
+
color = false;
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--color". Use "true" or "false"`, this.#getCommandPath());
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return color;
|
|
1120
|
+
}
|
|
926
1121
|
#hasFlag(tokens, longName, shortName) {
|
|
927
1122
|
for (const token of tokens) {
|
|
928
1123
|
if (token.type === 'long' && token.name === longName) {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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 */
|
|
@@ -212,7 +224,7 @@ interface ICommandParseResult {
|
|
|
212
224
|
rawArgs: string[];
|
|
213
225
|
}
|
|
214
226
|
/** Error kinds for command parsing */
|
|
215
|
-
type ICommanderErrorKind = 'InvalidOptionFormat' | 'InvalidNegativeOption' | 'NegativeOptionWithValue' | 'NegativeOptionType' | 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
227
|
+
type ICommanderErrorKind = 'InvalidOptionFormat' | 'InvalidNegativeOption' | 'NegativeOptionWithValue' | 'NegativeOptionType' | 'UnknownOption' | 'UnknownSubcommand' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
|
|
216
228
|
/** Commander error with structured information */
|
|
217
229
|
declare class CommanderError extends Error {
|
|
218
230
|
readonly kind: ICommanderErrorKind;
|
|
@@ -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 };
|