@guanghechen/commander 2.1.0 → 3.0.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 +7 -2
- package/lib/cjs/index.cjs +37 -55
- package/lib/esm/index.mjs +37 -55
- package/lib/types/index.d.ts +6 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See
|
|
4
4
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 3.0.0 (2026-02-08)
|
|
7
|
+
|
|
8
|
+
### BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
- Change subcommand registration API to `subcommand(name, cmd)`
|
|
11
|
+
|
|
12
|
+
## 2.1.0 (2026-02-08)
|
|
7
13
|
|
|
8
14
|
### Features
|
|
9
15
|
|
|
@@ -30,4 +36,3 @@ All notable changes to this project will be documented in this file. See
|
|
|
30
36
|
### Features
|
|
31
37
|
|
|
32
38
|
- Initial stable release: A minimal, type-safe command-line interface builder with fluent API
|
|
33
|
-
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -67,34 +67,25 @@ class Command {
|
|
|
67
67
|
#name;
|
|
68
68
|
#description;
|
|
69
69
|
#version;
|
|
70
|
-
#aliases;
|
|
71
70
|
#helpSubcommandEnabled;
|
|
72
71
|
#options = [];
|
|
73
72
|
#arguments = [];
|
|
74
73
|
#subcommands = [];
|
|
75
74
|
#action;
|
|
76
|
-
#parent;
|
|
77
75
|
constructor(config) {
|
|
78
|
-
this.#name = config.name;
|
|
76
|
+
this.#name = config.name ?? '';
|
|
79
77
|
this.#description = config.description;
|
|
80
78
|
this.#version = config.version;
|
|
81
|
-
this.#aliases = config.aliases ?? [];
|
|
82
79
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
83
80
|
}
|
|
84
81
|
get name() {
|
|
85
82
|
return this.#name;
|
|
86
83
|
}
|
|
87
|
-
get aliases() {
|
|
88
|
-
return this.#aliases;
|
|
89
|
-
}
|
|
90
84
|
get description() {
|
|
91
85
|
return this.#description;
|
|
92
86
|
}
|
|
93
87
|
get version() {
|
|
94
|
-
return this.#version
|
|
95
|
-
}
|
|
96
|
-
get parent() {
|
|
97
|
-
return this.#parent;
|
|
88
|
+
return this.#version;
|
|
98
89
|
}
|
|
99
90
|
get options() {
|
|
100
91
|
return [...this.#options];
|
|
@@ -117,12 +108,18 @@ class Command {
|
|
|
117
108
|
this.#action = fn;
|
|
118
109
|
return this;
|
|
119
110
|
}
|
|
120
|
-
subcommand(cmd) {
|
|
121
|
-
if (this.#helpSubcommandEnabled &&
|
|
111
|
+
subcommand(name, cmd) {
|
|
112
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
122
113
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
123
114
|
}
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
116
|
+
if (existing) {
|
|
117
|
+
existing.aliases.push(name);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
cmd.#name = name;
|
|
121
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
122
|
+
}
|
|
126
123
|
return this;
|
|
127
124
|
}
|
|
128
125
|
async run(params) {
|
|
@@ -313,12 +310,12 @@ class Command {
|
|
|
313
310
|
if (showHelpSubcommand) {
|
|
314
311
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
315
312
|
}
|
|
316
|
-
for (const
|
|
317
|
-
let name =
|
|
318
|
-
if (
|
|
319
|
-
name += `, ${
|
|
313
|
+
for (const entry of this.#subcommands) {
|
|
314
|
+
let name = entry.name;
|
|
315
|
+
if (entry.aliases.length > 0) {
|
|
316
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
320
317
|
}
|
|
321
|
-
cmdLines.push({ name, desc:
|
|
318
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
322
319
|
}
|
|
323
320
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
324
321
|
for (const { name, desc } of cmdLines) {
|
|
@@ -345,9 +342,16 @@ class Command {
|
|
|
345
342
|
return {
|
|
346
343
|
name: this.#name,
|
|
347
344
|
description: this.#description,
|
|
348
|
-
aliases:
|
|
345
|
+
aliases: [],
|
|
349
346
|
options,
|
|
350
|
-
subcommands: this.#subcommands.map(
|
|
347
|
+
subcommands: this.#subcommands.map(entry => {
|
|
348
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
349
|
+
return {
|
|
350
|
+
...subMeta,
|
|
351
|
+
name: entry.name,
|
|
352
|
+
aliases: entry.aliases,
|
|
353
|
+
};
|
|
354
|
+
}),
|
|
351
355
|
};
|
|
352
356
|
}
|
|
353
357
|
#processHelpSubcommand(argv) {
|
|
@@ -359,8 +363,8 @@ class Command {
|
|
|
359
363
|
return ['--help'];
|
|
360
364
|
}
|
|
361
365
|
const subName = argv[1];
|
|
362
|
-
const
|
|
363
|
-
if (
|
|
366
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
367
|
+
if (entry) {
|
|
364
368
|
return [subName, '--help', ...argv.slice(2)];
|
|
365
369
|
}
|
|
366
370
|
return argv;
|
|
@@ -372,10 +376,10 @@ class Command {
|
|
|
372
376
|
const token = argv[idx];
|
|
373
377
|
if (token.startsWith('-'))
|
|
374
378
|
break;
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
379
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
380
|
+
if (!entry)
|
|
377
381
|
break;
|
|
378
|
-
current =
|
|
382
|
+
current = entry.command;
|
|
379
383
|
idx += 1;
|
|
380
384
|
}
|
|
381
385
|
return { command: current, remaining: argv.slice(idx) };
|
|
@@ -489,33 +493,17 @@ class Command {
|
|
|
489
493
|
}
|
|
490
494
|
}
|
|
491
495
|
#getMergedOptions() {
|
|
492
|
-
const ancestors = [];
|
|
493
|
-
for (let node = this; node; node = node.#parent) {
|
|
494
|
-
ancestors.unshift(node);
|
|
495
|
-
}
|
|
496
496
|
const optionMap = new Map();
|
|
497
|
-
const hasUserHelp =
|
|
498
|
-
const hasUserVersion =
|
|
497
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
498
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
499
499
|
if (!hasUserHelp) {
|
|
500
500
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
501
501
|
}
|
|
502
502
|
if (!hasUserVersion) {
|
|
503
503
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
504
504
|
}
|
|
505
|
-
for (const
|
|
506
|
-
|
|
507
|
-
optionMap.set(opt.long, opt);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const shortToLong = new Map();
|
|
511
|
-
for (const [long, opt] of optionMap) {
|
|
512
|
-
if (opt.short) {
|
|
513
|
-
const existing = shortToLong.get(opt.short);
|
|
514
|
-
if (existing && existing !== long) {
|
|
515
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
516
|
-
}
|
|
517
|
-
shortToLong.set(opt.short, long);
|
|
518
|
-
}
|
|
505
|
+
for (const opt of this.#options) {
|
|
506
|
+
optionMap.set(opt.long, opt);
|
|
519
507
|
}
|
|
520
508
|
return Array.from(optionMap.values());
|
|
521
509
|
}
|
|
@@ -654,20 +642,15 @@ class Command {
|
|
|
654
642
|
return result;
|
|
655
643
|
}
|
|
656
644
|
#getCommandPath() {
|
|
657
|
-
|
|
658
|
-
for (let node = this; node; node = node.#parent) {
|
|
659
|
-
parts.unshift(node.#name);
|
|
660
|
-
}
|
|
661
|
-
return parts.join(' ');
|
|
645
|
+
return this.#name;
|
|
662
646
|
}
|
|
663
647
|
}
|
|
664
648
|
|
|
665
649
|
class CompletionCommand extends Command {
|
|
666
650
|
constructor(root, config) {
|
|
667
|
-
const name = config.name ?? 'completion';
|
|
668
651
|
const paths = config.paths;
|
|
652
|
+
const programName = config.programName ?? root.name;
|
|
669
653
|
super({
|
|
670
|
-
name,
|
|
671
654
|
description: 'Generate shell completion script',
|
|
672
655
|
});
|
|
673
656
|
this.option({
|
|
@@ -694,7 +677,6 @@ class CompletionCommand extends Command {
|
|
|
694
677
|
})
|
|
695
678
|
.action(({ opts }) => {
|
|
696
679
|
const meta = root.getCompletionMeta();
|
|
697
|
-
const programName = root.name;
|
|
698
680
|
const selectedShells = [
|
|
699
681
|
opts['bash'] && 'bash',
|
|
700
682
|
opts['fish'] && 'fish',
|
package/lib/esm/index.mjs
CHANGED
|
@@ -45,34 +45,25 @@ class Command {
|
|
|
45
45
|
#name;
|
|
46
46
|
#description;
|
|
47
47
|
#version;
|
|
48
|
-
#aliases;
|
|
49
48
|
#helpSubcommandEnabled;
|
|
50
49
|
#options = [];
|
|
51
50
|
#arguments = [];
|
|
52
51
|
#subcommands = [];
|
|
53
52
|
#action;
|
|
54
|
-
#parent;
|
|
55
53
|
constructor(config) {
|
|
56
|
-
this.#name = config.name;
|
|
54
|
+
this.#name = config.name ?? '';
|
|
57
55
|
this.#description = config.description;
|
|
58
56
|
this.#version = config.version;
|
|
59
|
-
this.#aliases = config.aliases ?? [];
|
|
60
57
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
61
58
|
}
|
|
62
59
|
get name() {
|
|
63
60
|
return this.#name;
|
|
64
61
|
}
|
|
65
|
-
get aliases() {
|
|
66
|
-
return this.#aliases;
|
|
67
|
-
}
|
|
68
62
|
get description() {
|
|
69
63
|
return this.#description;
|
|
70
64
|
}
|
|
71
65
|
get version() {
|
|
72
|
-
return this.#version
|
|
73
|
-
}
|
|
74
|
-
get parent() {
|
|
75
|
-
return this.#parent;
|
|
66
|
+
return this.#version;
|
|
76
67
|
}
|
|
77
68
|
get options() {
|
|
78
69
|
return [...this.#options];
|
|
@@ -95,12 +86,18 @@ class Command {
|
|
|
95
86
|
this.#action = fn;
|
|
96
87
|
return this;
|
|
97
88
|
}
|
|
98
|
-
subcommand(cmd) {
|
|
99
|
-
if (this.#helpSubcommandEnabled &&
|
|
89
|
+
subcommand(name, cmd) {
|
|
90
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
100
91
|
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.aliases.push(name);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
cmd.#name = name;
|
|
99
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
100
|
+
}
|
|
104
101
|
return this;
|
|
105
102
|
}
|
|
106
103
|
async run(params) {
|
|
@@ -291,12 +288,12 @@ class Command {
|
|
|
291
288
|
if (showHelpSubcommand) {
|
|
292
289
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
293
290
|
}
|
|
294
|
-
for (const
|
|
295
|
-
let name =
|
|
296
|
-
if (
|
|
297
|
-
name += `, ${
|
|
291
|
+
for (const entry of this.#subcommands) {
|
|
292
|
+
let name = entry.name;
|
|
293
|
+
if (entry.aliases.length > 0) {
|
|
294
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
298
295
|
}
|
|
299
|
-
cmdLines.push({ name, desc:
|
|
296
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
300
297
|
}
|
|
301
298
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
302
299
|
for (const { name, desc } of cmdLines) {
|
|
@@ -323,9 +320,16 @@ class Command {
|
|
|
323
320
|
return {
|
|
324
321
|
name: this.#name,
|
|
325
322
|
description: this.#description,
|
|
326
|
-
aliases:
|
|
323
|
+
aliases: [],
|
|
327
324
|
options,
|
|
328
|
-
subcommands: this.#subcommands.map(
|
|
325
|
+
subcommands: this.#subcommands.map(entry => {
|
|
326
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
327
|
+
return {
|
|
328
|
+
...subMeta,
|
|
329
|
+
name: entry.name,
|
|
330
|
+
aliases: entry.aliases,
|
|
331
|
+
};
|
|
332
|
+
}),
|
|
329
333
|
};
|
|
330
334
|
}
|
|
331
335
|
#processHelpSubcommand(argv) {
|
|
@@ -337,8 +341,8 @@ class Command {
|
|
|
337
341
|
return ['--help'];
|
|
338
342
|
}
|
|
339
343
|
const subName = argv[1];
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
344
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
345
|
+
if (entry) {
|
|
342
346
|
return [subName, '--help', ...argv.slice(2)];
|
|
343
347
|
}
|
|
344
348
|
return argv;
|
|
@@ -350,10 +354,10 @@ class Command {
|
|
|
350
354
|
const token = argv[idx];
|
|
351
355
|
if (token.startsWith('-'))
|
|
352
356
|
break;
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
357
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
358
|
+
if (!entry)
|
|
355
359
|
break;
|
|
356
|
-
current =
|
|
360
|
+
current = entry.command;
|
|
357
361
|
idx += 1;
|
|
358
362
|
}
|
|
359
363
|
return { command: current, remaining: argv.slice(idx) };
|
|
@@ -467,33 +471,17 @@ class Command {
|
|
|
467
471
|
}
|
|
468
472
|
}
|
|
469
473
|
#getMergedOptions() {
|
|
470
|
-
const ancestors = [];
|
|
471
|
-
for (let node = this; node; node = node.#parent) {
|
|
472
|
-
ancestors.unshift(node);
|
|
473
|
-
}
|
|
474
474
|
const optionMap = new Map();
|
|
475
|
-
const hasUserHelp =
|
|
476
|
-
const hasUserVersion =
|
|
475
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
476
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
477
477
|
if (!hasUserHelp) {
|
|
478
478
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
479
479
|
}
|
|
480
480
|
if (!hasUserVersion) {
|
|
481
481
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
482
482
|
}
|
|
483
|
-
for (const
|
|
484
|
-
|
|
485
|
-
optionMap.set(opt.long, opt);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
const shortToLong = new Map();
|
|
489
|
-
for (const [long, opt] of optionMap) {
|
|
490
|
-
if (opt.short) {
|
|
491
|
-
const existing = shortToLong.get(opt.short);
|
|
492
|
-
if (existing && existing !== long) {
|
|
493
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
494
|
-
}
|
|
495
|
-
shortToLong.set(opt.short, long);
|
|
496
|
-
}
|
|
483
|
+
for (const opt of this.#options) {
|
|
484
|
+
optionMap.set(opt.long, opt);
|
|
497
485
|
}
|
|
498
486
|
return Array.from(optionMap.values());
|
|
499
487
|
}
|
|
@@ -632,20 +620,15 @@ class Command {
|
|
|
632
620
|
return result;
|
|
633
621
|
}
|
|
634
622
|
#getCommandPath() {
|
|
635
|
-
|
|
636
|
-
for (let node = this; node; node = node.#parent) {
|
|
637
|
-
parts.unshift(node.#name);
|
|
638
|
-
}
|
|
639
|
-
return parts.join(' ');
|
|
623
|
+
return this.#name;
|
|
640
624
|
}
|
|
641
625
|
}
|
|
642
626
|
|
|
643
627
|
class CompletionCommand extends Command {
|
|
644
628
|
constructor(root, config) {
|
|
645
|
-
const name = config.name ?? 'completion';
|
|
646
629
|
const paths = config.paths;
|
|
630
|
+
const programName = config.programName ?? root.name;
|
|
647
631
|
super({
|
|
648
|
-
name,
|
|
649
632
|
description: 'Generate shell completion script',
|
|
650
633
|
});
|
|
651
634
|
this.option({
|
|
@@ -672,7 +655,6 @@ class CompletionCommand extends Command {
|
|
|
672
655
|
})
|
|
673
656
|
.action(({ opts }) => {
|
|
674
657
|
const meta = root.getCompletionMeta();
|
|
675
|
-
const programName = root.name;
|
|
676
658
|
const selectedShells = [
|
|
677
659
|
opts['bash'] && 'bash',
|
|
678
660
|
opts['fish'] && 'fish',
|
package/lib/types/index.d.ts
CHANGED
|
@@ -57,10 +57,8 @@ interface IArgument {
|
|
|
57
57
|
}
|
|
58
58
|
/** Command configuration */
|
|
59
59
|
interface ICommandConfig {
|
|
60
|
-
/** Command name (
|
|
61
|
-
name
|
|
62
|
-
/** Command aliases */
|
|
63
|
-
aliases?: string[];
|
|
60
|
+
/** Command name (only effective for root command) */
|
|
61
|
+
name?: string;
|
|
64
62
|
/** Command description */
|
|
65
63
|
description: string;
|
|
66
64
|
/** Version (only effective for root command) */
|
|
@@ -71,10 +69,8 @@ interface ICommandConfig {
|
|
|
71
69
|
/** Forward declaration for Command class */
|
|
72
70
|
interface ICommand {
|
|
73
71
|
readonly name: string;
|
|
74
|
-
readonly aliases: string[];
|
|
75
72
|
readonly description: string;
|
|
76
73
|
readonly version: string | undefined;
|
|
77
|
-
readonly parent: ICommand | undefined;
|
|
78
74
|
readonly options: IOption[];
|
|
79
75
|
readonly arguments: IArgument[];
|
|
80
76
|
}
|
|
@@ -155,8 +151,8 @@ interface ICompletionPaths {
|
|
|
155
151
|
}
|
|
156
152
|
/** CompletionCommand configuration */
|
|
157
153
|
interface ICompletionCommandConfig {
|
|
158
|
-
/**
|
|
159
|
-
|
|
154
|
+
/** Program name for completion scripts (defaults to root.name) */
|
|
155
|
+
programName?: string;
|
|
160
156
|
/** Default completion file paths for each shell (required for --write support) */
|
|
161
157
|
paths: ICompletionPaths;
|
|
162
158
|
}
|
|
@@ -171,16 +167,14 @@ declare class Command implements ICommand {
|
|
|
171
167
|
#private;
|
|
172
168
|
constructor(config: ICommandConfig);
|
|
173
169
|
get name(): string;
|
|
174
|
-
get aliases(): string[];
|
|
175
170
|
get description(): string;
|
|
176
171
|
get version(): string | undefined;
|
|
177
|
-
get parent(): Command | undefined;
|
|
178
172
|
get options(): IOption[];
|
|
179
173
|
get arguments(): IArgument[];
|
|
180
174
|
option(opt: IOption): this;
|
|
181
175
|
argument(arg: IArgument): this;
|
|
182
176
|
action(fn: IAction): this;
|
|
183
|
-
subcommand(cmd: Command): this;
|
|
177
|
+
subcommand(name: string, cmd: Command): this;
|
|
184
178
|
run(params: IRunParams): Promise<void>;
|
|
185
179
|
parse(argv: string[]): IParseResult;
|
|
186
180
|
formatHelp(): string;
|
|
@@ -199,7 +193,7 @@ declare class Command implements ICommand {
|
|
|
199
193
|
* @example
|
|
200
194
|
* ```typescript
|
|
201
195
|
* const root = new Command({ name: 'mycli', description: 'My CLI' })
|
|
202
|
-
* root.subcommand(new CompletionCommand(root, {
|
|
196
|
+
* root.subcommand('completion', new CompletionCommand(root, {
|
|
203
197
|
* paths: {
|
|
204
198
|
* bash: `~/.local/share/bash-completion/completions/mycli`,
|
|
205
199
|
* fish: `~/.config/fish/completions/mycli.fish`,
|