@guanghechen/commander 3.1.0 → 3.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,25 @@
1
1
  # Change Log
2
2
 
3
+ ## 3.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Apply code review fixes:
8
+ - Fix `#getCommandPath` to return full command path (e.g., "cli sub" instead of just "sub")
9
+ - Remove unused `#parseLongOption` and `#parseShortOption` dead code
10
+ - Improve `--no-{option}` help description to "Negate --{option}"
11
+ - Document short option negative value limitation in spec
12
+
13
+ ## 3.2.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Change args from string[] to Record<string, unknown> with type/coerce/default support:
18
+ - `args` is now `Record<string, unknown>` keyed by argument name
19
+ - Add `rawArgs: string[]` for original argument strings before type conversion
20
+ - IArgument now supports `type`, `default`, and `coerce` properties
21
+ - Add `TooManyArguments` error kind for extra arguments validation
22
+
3
23
  ## 3.1.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -124,7 +124,6 @@ const root = new Command({
124
124
  })
125
125
 
126
126
  const clone = new Command({
127
- name: 'clone',
128
127
  description: 'Clone a repository',
129
128
  })
130
129
  .argument({ name: 'url', kind: 'required', description: 'Repository URL' })
@@ -134,8 +133,6 @@ const clone = new Command({
134
133
  })
135
134
 
136
135
  const commit = new Command({
137
- name: 'commit',
138
- aliases: ['ci'],
139
136
  description: 'Record changes to the repository',
140
137
  })
141
138
  .option({ long: 'message', short: 'm', type: 'string', required: true, description: 'Commit message' })
@@ -144,7 +141,7 @@ const commit = new Command({
144
141
  console.log(`Committing: ${opts['message']}`)
145
142
  })
146
143
 
147
- root.subcommand(clone).subcommand(commit)
144
+ root.subcommand('clone', clone).subcommand('commit', commit).subcommand('ci', commit)
148
145
 
149
146
  root.run({ argv: process.argv.slice(2), envs: process.env })
150
147
  ```
@@ -161,7 +158,7 @@ const root = new Command({
161
158
  })
162
159
 
163
160
  // Add completion subcommand
164
- root.subcommand(new CompletionCommand(root))
161
+ root.subcommand('completion', new CompletionCommand(root))
165
162
 
166
163
  // Generate completion scripts:
167
164
  // mycli completion --bash > ~/.local/share/bash-completion/completions/mycli
package/lib/cjs/index.cjs CHANGED
@@ -68,6 +68,8 @@ class Command {
68
68
  #description;
69
69
  #version;
70
70
  #helpSubcommandEnabled;
71
+ #reporter;
72
+ #parent;
71
73
  #options = [];
72
74
  #arguments = [];
73
75
  #subcommands = [];
@@ -77,6 +79,7 @@ class Command {
77
79
  this.#description = config.description;
78
80
  this.#version = config.version;
79
81
  this.#helpSubcommandEnabled = config.help ?? false;
82
+ this.#reporter = config.reporter;
80
83
  }
81
84
  get name() {
82
85
  return this.#name;
@@ -87,6 +90,9 @@ class Command {
87
90
  get version() {
88
91
  return this.#version;
89
92
  }
93
+ get parent() {
94
+ return this.#parent;
95
+ }
90
96
  get options() {
91
97
  return [...this.#options];
92
98
  }
@@ -112,12 +118,16 @@ class Command {
112
118
  if (this.#helpSubcommandEnabled && name === 'help') {
113
119
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
114
120
  }
121
+ if (cmd.#parent && cmd.#parent !== this) {
122
+ throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
123
+ }
115
124
  const existing = this.#subcommands.find(e => e.command === cmd);
116
125
  if (existing) {
117
126
  existing.aliases.push(name);
118
127
  }
119
128
  else {
120
129
  cmd.#name = name;
130
+ cmd.#parent = this;
121
131
  this.#subcommands.push({ name, aliases: [], command: cmd });
122
132
  }
123
133
  return this;
@@ -128,34 +138,35 @@ class Command {
128
138
  const processedArgv = this.#processHelpSubcommand(argv);
129
139
  const { chain, remaining } = this.#routeChain(processedArgv);
130
140
  const leafCommand = chain[chain.length - 1];
141
+ const rootCommand = chain[0];
142
+ const includeRootVersion = chain.length === 1;
143
+ this.#validateMergedShortOptions(chain, includeRootVersion);
131
144
  const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
132
- const leafOptions = leafCommand.#getMergedOptions();
145
+ const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
133
146
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
134
147
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
135
148
  if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
136
149
  console.log(leafCommand.formatHelp());
137
150
  return;
138
151
  }
139
- if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
140
- console.log(leafCommand.version ?? 'unknown');
141
- return;
152
+ if (!hasUserVersion && leafCommand === rootCommand) {
153
+ if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
154
+ console.log(leafCommand.version ?? 'unknown');
155
+ return;
156
+ }
142
157
  }
143
- const optsMap = this.#shiftChain(chain, optionTokens);
158
+ const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
144
159
  const ctx = {
145
160
  cmd: leafCommand,
146
161
  envs,
147
- reporter: reporter ?? new DefaultReporter(),
162
+ reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
148
163
  argv,
149
164
  };
150
165
  this.#applyChain(chain, optsMap, ctx);
151
166
  const mergedOpts = this.#mergeOpts(chain, optsMap);
152
- const args = restArgs;
153
- const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
154
- if (args.length < requiredArgs.length) {
155
- const missing = requiredArgs.slice(args.length).map(a => a.name);
156
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
157
- }
158
- const actionParams = { ctx, opts: mergedOpts, args };
167
+ const allArgs = [...positionalArgs, ...restArgs];
168
+ const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
169
+ const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
159
170
  if (leafCommand.#action) {
160
171
  try {
161
172
  await leafCommand.#action(actionParams);
@@ -187,76 +198,23 @@ class Command {
187
198
  }
188
199
  }
189
200
  parse(argv) {
190
- const allOptions = this.#getMergedOptions();
191
- const opts = {};
192
- const args = [];
193
- for (const opt of allOptions) {
194
- if (opt.default !== undefined) {
195
- opts[opt.long] = opt.default;
196
- }
197
- else if (opt.type === 'boolean') {
198
- opts[opt.long] = false;
199
- }
200
- else if (opt.type === 'string[]' || opt.type === 'number[]') {
201
- opts[opt.long] = [];
202
- }
203
- }
204
- let remaining = [...argv];
205
- const resolverOptions = allOptions.filter(o => o.resolver);
206
- for (const opt of resolverOptions) {
207
- const result = opt.resolver(remaining);
208
- opts[opt.long] = result.value;
209
- remaining = result.remaining;
210
- }
211
- const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
212
- remaining = this.#normalizeArgv(remaining, booleanOptions);
213
- let i = 0;
214
- while (i < remaining.length) {
215
- const token = remaining[i];
216
- if (token === '--') {
217
- args.push(...remaining.slice(i + 1));
218
- break;
219
- }
220
- if (token.startsWith('--')) {
221
- i = this.#parseLongOption(remaining, i, optionByLong, opts);
222
- continue;
223
- }
224
- if (token.startsWith('-') && token.length > 1) {
225
- i = this.#parseShortOption(remaining, i, optionByShort, opts);
226
- continue;
227
- }
228
- args.push(token);
229
- i += 1;
230
- }
231
- for (const opt of allOptions) {
232
- if (opt.required && opts[opt.long] === undefined) {
233
- throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
234
- }
235
- }
236
- for (const opt of allOptions) {
237
- if (opt.choices && opts[opt.long] !== undefined) {
238
- const value = opts[opt.long];
239
- const values = Array.isArray(value) ? value : [value];
240
- const choices = opt.choices;
241
- for (const v of values) {
242
- if (!choices.includes(v)) {
243
- throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
244
- }
245
- }
246
- }
247
- }
248
- const requiredArgs = this.#arguments.filter(a => a.kind === 'required');
249
- if (args.length < requiredArgs.length) {
250
- const missing = requiredArgs.slice(args.length).map(a => a.name);
251
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
252
- }
253
- return { opts, args };
201
+ const processedArgv = this.#processHelpSubcommand(argv);
202
+ const { chain, remaining } = this.#routeChain(processedArgv);
203
+ const leafCommand = chain[chain.length - 1];
204
+ const includeRootVersion = chain.length === 1;
205
+ this.#validateMergedShortOptions(chain, includeRootVersion);
206
+ const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
207
+ const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
208
+ const mergedOpts = this.#mergeOpts(chain, optsMap);
209
+ const allArgs = [...positionalArgs, ...restArgs];
210
+ const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
211
+ return { opts: mergedOpts, args, rawArgs };
254
212
  }
255
213
  shift(tokens) {
256
214
  return this.#shiftWithShadowed(tokens, new Set());
257
215
  }
258
- #shiftWithShadowed(tokens, shadowed) {
259
- const allDirectOptions = this.#getMergedOptions();
216
+ #shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
217
+ const allDirectOptions = this.#getMergedOptions(includeVersion);
260
218
  const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
261
219
  const opts = {};
262
220
  for (const opt of directOptions) {
@@ -373,7 +331,7 @@ class Command {
373
331
  if (effectiveType === 'boolean') {
374
332
  optLines.push({
375
333
  sig: ` --no-${opt.long}`,
376
- desc: opt.description,
334
+ desc: `Negate --${opt.long}`,
377
335
  });
378
336
  }
379
337
  }
@@ -436,11 +394,11 @@ class Command {
436
394
  };
437
395
  }
438
396
  #processHelpSubcommand(argv) {
439
- if (!this.#helpSubcommandEnabled || this.#subcommands.length === 0)
397
+ if (!this.#helpSubcommandEnabled)
440
398
  return argv;
441
399
  if (argv.length < 1 || argv[0] !== 'help')
442
400
  return argv;
443
- if (argv.length === 1) {
401
+ if (argv.length === 1 || this.#subcommands.length === 0) {
444
402
  return ['--help'];
445
403
  }
446
404
  const subName = argv[1];
@@ -477,30 +435,34 @@ class Command {
477
435
  restArgs: tokens.slice(ddIdx + 1),
478
436
  };
479
437
  }
480
- #shiftChain(chain, tokens) {
438
+ #shiftChain(chain, tokens, includeRootVersion) {
481
439
  const optsMap = new Map();
482
440
  let remaining = [...tokens];
441
+ const rootCommand = chain[0];
483
442
  const shadowed = new Set();
484
443
  for (let i = chain.length - 1; i >= 0; i--) {
485
444
  const cmd = chain[i];
486
- const result = cmd.#shiftWithShadowed(remaining, shadowed);
445
+ const includeVersion = cmd === rootCommand && includeRootVersion;
446
+ const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
487
447
  optsMap.set(cmd, result.opts);
488
448
  remaining = result.remaining;
489
449
  for (const opt of cmd.#options) {
490
450
  shadowed.add(opt.long);
491
451
  }
492
452
  }
493
- if (remaining.length > 0) {
494
- const leafCommand = chain[chain.length - 1];
495
- const firstToken = remaining[0];
496
- if (firstToken.startsWith('-')) {
497
- throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
498
- }
499
- else {
500
- throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
453
+ const positionalArgs = [];
454
+ for (const token of remaining) {
455
+ if (token.startsWith('-')) {
456
+ const leafCommand = chain[chain.length - 1];
457
+ if (!token.startsWith('--') && token.length > 2) {
458
+ const flag = token[1];
459
+ throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
460
+ }
461
+ throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
501
462
  }
463
+ positionalArgs.push(token);
502
464
  }
503
- return optsMap;
465
+ return { optsMap, positionalArgs };
504
466
  }
505
467
  #applyChain(chain, optsMap, ctx) {
506
468
  for (const cmd of chain) {
@@ -519,82 +481,6 @@ class Command {
519
481
  }
520
482
  return merged;
521
483
  }
522
- #parseLongOption(argv, idx, optionByLong, opts) {
523
- const token = argv[idx];
524
- const eqIdx = token.indexOf('=');
525
- let optName;
526
- let inlineValue;
527
- if (eqIdx !== -1) {
528
- optName = token.slice(2, eqIdx);
529
- inlineValue = token.slice(eqIdx + 1);
530
- }
531
- else {
532
- optName = token.slice(2);
533
- }
534
- const opt = optionByLong.get(optName);
535
- if (!opt) {
536
- throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
537
- }
538
- if (opt.type === 'boolean') {
539
- if (inlineValue !== undefined) {
540
- if (inlineValue === 'true') {
541
- opts[optName] = true;
542
- }
543
- else if (inlineValue === 'false') {
544
- opts[optName] = false;
545
- }
546
- else {
547
- throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
548
- }
549
- }
550
- else {
551
- opts[optName] = true;
552
- }
553
- return idx + 1;
554
- }
555
- let value;
556
- let nextIdx = idx;
557
- if (inlineValue !== undefined) {
558
- value = inlineValue;
559
- }
560
- else if (idx + 1 < argv.length) {
561
- value = argv[idx + 1];
562
- nextIdx += 1;
563
- }
564
- else {
565
- throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
566
- }
567
- this.#applyValue(opt, value, opts);
568
- return nextIdx + 1;
569
- }
570
- #parseShortOption(argv, idx, optionByShort, opts) {
571
- const token = argv[idx];
572
- if (token.includes('=')) {
573
- throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
574
- }
575
- const flags = token.slice(1);
576
- for (let j = 0; j < flags.length; j++) {
577
- const flag = flags[j];
578
- const opt = optionByShort.get(flag);
579
- if (!opt) {
580
- throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
581
- }
582
- if (opt.type === 'boolean') {
583
- opts[opt.long] = true;
584
- continue;
585
- }
586
- if (j < flags.length - 1) {
587
- throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
588
- }
589
- if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
590
- const value = argv[idx + 1];
591
- this.#applyValue(opt, value, opts);
592
- return idx + 2;
593
- }
594
- throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
595
- }
596
- return idx + 1;
597
- }
598
484
  #applyValue(opt, rawValue, opts) {
599
485
  const type = opt.type ?? 'string';
600
486
  let parsedValue = rawValue;
@@ -627,14 +513,14 @@ class Command {
627
513
  opts[opt.long] = parsedValue;
628
514
  }
629
515
  }
630
- #getMergedOptions() {
516
+ #getMergedOptions(includeVersion = !this.#parent) {
631
517
  const optionMap = new Map();
632
518
  const hasUserHelp = this.#options.some(o => o.long === 'help');
633
519
  const hasUserVersion = this.#options.some(o => o.long === 'version');
634
520
  if (!hasUserHelp) {
635
521
  optionMap.set('help', BUILTIN_HELP_OPTION);
636
522
  }
637
- if (!hasUserVersion) {
523
+ if (!hasUserVersion && includeVersion) {
638
524
  optionMap.set('version', BUILTIN_VERSION_OPTION);
639
525
  }
640
526
  for (const opt of this.#options) {
@@ -642,6 +528,26 @@ class Command {
642
528
  }
643
529
  return Array.from(optionMap.values());
644
530
  }
531
+ #validateMergedShortOptions(chain, includeRootVersion) {
532
+ const mergedByLong = new Map();
533
+ const rootCommand = chain[0];
534
+ for (const cmd of chain) {
535
+ const includeVersion = cmd === rootCommand && includeRootVersion;
536
+ for (const opt of cmd.#getMergedOptions(includeVersion)) {
537
+ mergedByLong.set(opt.long, opt);
538
+ }
539
+ }
540
+ const shortMap = new Map();
541
+ for (const opt of mergedByLong.values()) {
542
+ if (!opt.short)
543
+ continue;
544
+ const existingLong = shortMap.get(opt.short);
545
+ if (existingLong && existingLong !== opt.long) {
546
+ throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
547
+ }
548
+ shortMap.set(opt.short, opt.long);
549
+ }
550
+ }
645
551
  #validateOptionConfig(opt) {
646
552
  if (opt.long.startsWith('no-')) {
647
553
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
@@ -662,6 +568,9 @@ class Command {
662
568
  }
663
569
  }
664
570
  #validateArgumentConfig(arg) {
571
+ if (arg.kind === 'required' && arg.default !== undefined) {
572
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
573
+ }
665
574
  if (arg.kind === 'variadic') {
666
575
  if (this.#arguments.some(a => a.kind === 'variadic')) {
667
576
  throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
@@ -680,6 +589,61 @@ class Command {
680
589
  }
681
590
  }
682
591
  }
592
+ #parseArguments(rawArgs) {
593
+ const argumentDefs = this.#arguments;
594
+ const args = {};
595
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
596
+ if (rawArgs.length < requiredCount) {
597
+ const missing = argumentDefs
598
+ .filter(a => a.kind === 'required')
599
+ .slice(rawArgs.length)
600
+ .map(a => a.name);
601
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
602
+ }
603
+ let index = 0;
604
+ for (const def of argumentDefs) {
605
+ if (def.kind === 'variadic') {
606
+ const rest = rawArgs.slice(index);
607
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
608
+ index = rawArgs.length;
609
+ break;
610
+ }
611
+ const raw = rawArgs[index];
612
+ if (raw === undefined) {
613
+ if (def.kind === 'optional') {
614
+ args[def.name] = def.default ?? undefined;
615
+ continue;
616
+ }
617
+ }
618
+ else {
619
+ args[def.name] = this.#convertArgument(def, raw);
620
+ index += 1;
621
+ }
622
+ }
623
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
624
+ if (!hasVariadic && index < rawArgs.length) {
625
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
626
+ }
627
+ return { args, rawArgs };
628
+ }
629
+ #convertArgument(def, raw) {
630
+ if (def.coerce) {
631
+ try {
632
+ return def.coerce(raw);
633
+ }
634
+ catch {
635
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
636
+ }
637
+ }
638
+ if (def.type === 'number') {
639
+ const n = Number(raw);
640
+ if (Number.isNaN(n)) {
641
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
642
+ }
643
+ return n;
644
+ }
645
+ return raw;
646
+ }
683
647
  #buildOptionMaps(allOptions, excludeResolver = false) {
684
648
  const optionByLong = new Map();
685
649
  const optionByShort = new Map();
@@ -774,7 +738,15 @@ class Command {
774
738
  return result;
775
739
  }
776
740
  #getCommandPath() {
777
- return this.#name;
741
+ const parts = [];
742
+ let current = this;
743
+ while (current) {
744
+ if (current.#name) {
745
+ parts.unshift(current.#name);
746
+ }
747
+ current = current.#parent;
748
+ }
749
+ return parts.join(' ') || this.#name;
778
750
  }
779
751
  #tryConsumeLongOption(tokens, idx, optionByLong, opts) {
780
752
  const token = tokens[idx];
package/lib/esm/index.mjs CHANGED
@@ -46,6 +46,8 @@ class Command {
46
46
  #description;
47
47
  #version;
48
48
  #helpSubcommandEnabled;
49
+ #reporter;
50
+ #parent;
49
51
  #options = [];
50
52
  #arguments = [];
51
53
  #subcommands = [];
@@ -55,6 +57,7 @@ class Command {
55
57
  this.#description = config.description;
56
58
  this.#version = config.version;
57
59
  this.#helpSubcommandEnabled = config.help ?? false;
60
+ this.#reporter = config.reporter;
58
61
  }
59
62
  get name() {
60
63
  return this.#name;
@@ -65,6 +68,9 @@ class Command {
65
68
  get version() {
66
69
  return this.#version;
67
70
  }
71
+ get parent() {
72
+ return this.#parent;
73
+ }
68
74
  get options() {
69
75
  return [...this.#options];
70
76
  }
@@ -90,12 +96,16 @@ class Command {
90
96
  if (this.#helpSubcommandEnabled && name === 'help') {
91
97
  throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
92
98
  }
99
+ if (cmd.#parent && cmd.#parent !== this) {
100
+ throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
101
+ }
93
102
  const existing = this.#subcommands.find(e => e.command === cmd);
94
103
  if (existing) {
95
104
  existing.aliases.push(name);
96
105
  }
97
106
  else {
98
107
  cmd.#name = name;
108
+ cmd.#parent = this;
99
109
  this.#subcommands.push({ name, aliases: [], command: cmd });
100
110
  }
101
111
  return this;
@@ -106,34 +116,35 @@ class Command {
106
116
  const processedArgv = this.#processHelpSubcommand(argv);
107
117
  const { chain, remaining } = this.#routeChain(processedArgv);
108
118
  const leafCommand = chain[chain.length - 1];
119
+ const rootCommand = chain[0];
120
+ const includeRootVersion = chain.length === 1;
121
+ this.#validateMergedShortOptions(chain, includeRootVersion);
109
122
  const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
110
- const leafOptions = leafCommand.#getMergedOptions();
123
+ const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
111
124
  const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
112
125
  const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
113
126
  if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
114
127
  console.log(leafCommand.formatHelp());
115
128
  return;
116
129
  }
117
- if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
118
- console.log(leafCommand.version ?? 'unknown');
119
- return;
130
+ if (!hasUserVersion && leafCommand === rootCommand) {
131
+ if (leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
132
+ console.log(leafCommand.version ?? 'unknown');
133
+ return;
134
+ }
120
135
  }
121
- const optsMap = this.#shiftChain(chain, optionTokens);
136
+ const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
122
137
  const ctx = {
123
138
  cmd: leafCommand,
124
139
  envs,
125
- reporter: reporter ?? new DefaultReporter(),
140
+ reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
126
141
  argv,
127
142
  };
128
143
  this.#applyChain(chain, optsMap, ctx);
129
144
  const mergedOpts = this.#mergeOpts(chain, optsMap);
130
- const args = restArgs;
131
- const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
132
- if (args.length < requiredArgs.length) {
133
- const missing = requiredArgs.slice(args.length).map(a => a.name);
134
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
135
- }
136
- const actionParams = { ctx, opts: mergedOpts, args };
145
+ const allArgs = [...positionalArgs, ...restArgs];
146
+ const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
147
+ const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
137
148
  if (leafCommand.#action) {
138
149
  try {
139
150
  await leafCommand.#action(actionParams);
@@ -165,76 +176,23 @@ class Command {
165
176
  }
166
177
  }
167
178
  parse(argv) {
168
- const allOptions = this.#getMergedOptions();
169
- const opts = {};
170
- const args = [];
171
- for (const opt of allOptions) {
172
- if (opt.default !== undefined) {
173
- opts[opt.long] = opt.default;
174
- }
175
- else if (opt.type === 'boolean') {
176
- opts[opt.long] = false;
177
- }
178
- else if (opt.type === 'string[]' || opt.type === 'number[]') {
179
- opts[opt.long] = [];
180
- }
181
- }
182
- let remaining = [...argv];
183
- const resolverOptions = allOptions.filter(o => o.resolver);
184
- for (const opt of resolverOptions) {
185
- const result = opt.resolver(remaining);
186
- opts[opt.long] = result.value;
187
- remaining = result.remaining;
188
- }
189
- const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
190
- remaining = this.#normalizeArgv(remaining, booleanOptions);
191
- let i = 0;
192
- while (i < remaining.length) {
193
- const token = remaining[i];
194
- if (token === '--') {
195
- args.push(...remaining.slice(i + 1));
196
- break;
197
- }
198
- if (token.startsWith('--')) {
199
- i = this.#parseLongOption(remaining, i, optionByLong, opts);
200
- continue;
201
- }
202
- if (token.startsWith('-') && token.length > 1) {
203
- i = this.#parseShortOption(remaining, i, optionByShort, opts);
204
- continue;
205
- }
206
- args.push(token);
207
- i += 1;
208
- }
209
- for (const opt of allOptions) {
210
- if (opt.required && opts[opt.long] === undefined) {
211
- throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
212
- }
213
- }
214
- for (const opt of allOptions) {
215
- if (opt.choices && opts[opt.long] !== undefined) {
216
- const value = opts[opt.long];
217
- const values = Array.isArray(value) ? value : [value];
218
- const choices = opt.choices;
219
- for (const v of values) {
220
- if (!choices.includes(v)) {
221
- throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
222
- }
223
- }
224
- }
225
- }
226
- const requiredArgs = this.#arguments.filter(a => a.kind === 'required');
227
- if (args.length < requiredArgs.length) {
228
- const missing = requiredArgs.slice(args.length).map(a => a.name);
229
- throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
230
- }
231
- return { opts, args };
179
+ const processedArgv = this.#processHelpSubcommand(argv);
180
+ const { chain, remaining } = this.#routeChain(processedArgv);
181
+ const leafCommand = chain[chain.length - 1];
182
+ const includeRootVersion = chain.length === 1;
183
+ this.#validateMergedShortOptions(chain, includeRootVersion);
184
+ const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
185
+ const { optsMap, positionalArgs } = this.#shiftChain(chain, optionTokens, includeRootVersion);
186
+ const mergedOpts = this.#mergeOpts(chain, optsMap);
187
+ const allArgs = [...positionalArgs, ...restArgs];
188
+ const { args, rawArgs } = leafCommand.#parseArguments(allArgs);
189
+ return { opts: mergedOpts, args, rawArgs };
232
190
  }
233
191
  shift(tokens) {
234
192
  return this.#shiftWithShadowed(tokens, new Set());
235
193
  }
236
- #shiftWithShadowed(tokens, shadowed) {
237
- const allDirectOptions = this.#getMergedOptions();
194
+ #shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
195
+ const allDirectOptions = this.#getMergedOptions(includeVersion);
238
196
  const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
239
197
  const opts = {};
240
198
  for (const opt of directOptions) {
@@ -351,7 +309,7 @@ class Command {
351
309
  if (effectiveType === 'boolean') {
352
310
  optLines.push({
353
311
  sig: ` --no-${opt.long}`,
354
- desc: opt.description,
312
+ desc: `Negate --${opt.long}`,
355
313
  });
356
314
  }
357
315
  }
@@ -414,11 +372,11 @@ class Command {
414
372
  };
415
373
  }
416
374
  #processHelpSubcommand(argv) {
417
- if (!this.#helpSubcommandEnabled || this.#subcommands.length === 0)
375
+ if (!this.#helpSubcommandEnabled)
418
376
  return argv;
419
377
  if (argv.length < 1 || argv[0] !== 'help')
420
378
  return argv;
421
- if (argv.length === 1) {
379
+ if (argv.length === 1 || this.#subcommands.length === 0) {
422
380
  return ['--help'];
423
381
  }
424
382
  const subName = argv[1];
@@ -455,30 +413,34 @@ class Command {
455
413
  restArgs: tokens.slice(ddIdx + 1),
456
414
  };
457
415
  }
458
- #shiftChain(chain, tokens) {
416
+ #shiftChain(chain, tokens, includeRootVersion) {
459
417
  const optsMap = new Map();
460
418
  let remaining = [...tokens];
419
+ const rootCommand = chain[0];
461
420
  const shadowed = new Set();
462
421
  for (let i = chain.length - 1; i >= 0; i--) {
463
422
  const cmd = chain[i];
464
- const result = cmd.#shiftWithShadowed(remaining, shadowed);
423
+ const includeVersion = cmd === rootCommand && includeRootVersion;
424
+ const result = cmd.#shiftWithShadowed(remaining, shadowed, includeVersion);
465
425
  optsMap.set(cmd, result.opts);
466
426
  remaining = result.remaining;
467
427
  for (const opt of cmd.#options) {
468
428
  shadowed.add(opt.long);
469
429
  }
470
430
  }
471
- if (remaining.length > 0) {
472
- const leafCommand = chain[chain.length - 1];
473
- const firstToken = remaining[0];
474
- if (firstToken.startsWith('-')) {
475
- throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
476
- }
477
- else {
478
- throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
431
+ const positionalArgs = [];
432
+ for (const token of remaining) {
433
+ if (token.startsWith('-')) {
434
+ const leafCommand = chain[chain.length - 1];
435
+ if (!token.startsWith('--') && token.length > 2) {
436
+ const flag = token[1];
437
+ throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
438
+ }
439
+ throw new CommanderError('UnknownOption', `unknown option "${token}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
479
440
  }
441
+ positionalArgs.push(token);
480
442
  }
481
- return optsMap;
443
+ return { optsMap, positionalArgs };
482
444
  }
483
445
  #applyChain(chain, optsMap, ctx) {
484
446
  for (const cmd of chain) {
@@ -497,82 +459,6 @@ class Command {
497
459
  }
498
460
  return merged;
499
461
  }
500
- #parseLongOption(argv, idx, optionByLong, opts) {
501
- const token = argv[idx];
502
- const eqIdx = token.indexOf('=');
503
- let optName;
504
- let inlineValue;
505
- if (eqIdx !== -1) {
506
- optName = token.slice(2, eqIdx);
507
- inlineValue = token.slice(eqIdx + 1);
508
- }
509
- else {
510
- optName = token.slice(2);
511
- }
512
- const opt = optionByLong.get(optName);
513
- if (!opt) {
514
- throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
515
- }
516
- if (opt.type === 'boolean') {
517
- if (inlineValue !== undefined) {
518
- if (inlineValue === 'true') {
519
- opts[optName] = true;
520
- }
521
- else if (inlineValue === 'false') {
522
- opts[optName] = false;
523
- }
524
- else {
525
- throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
526
- }
527
- }
528
- else {
529
- opts[optName] = true;
530
- }
531
- return idx + 1;
532
- }
533
- let value;
534
- let nextIdx = idx;
535
- if (inlineValue !== undefined) {
536
- value = inlineValue;
537
- }
538
- else if (idx + 1 < argv.length) {
539
- value = argv[idx + 1];
540
- nextIdx += 1;
541
- }
542
- else {
543
- throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
544
- }
545
- this.#applyValue(opt, value, opts);
546
- return nextIdx + 1;
547
- }
548
- #parseShortOption(argv, idx, optionByShort, opts) {
549
- const token = argv[idx];
550
- if (token.includes('=')) {
551
- throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
552
- }
553
- const flags = token.slice(1);
554
- for (let j = 0; j < flags.length; j++) {
555
- const flag = flags[j];
556
- const opt = optionByShort.get(flag);
557
- if (!opt) {
558
- throw new CommanderError('UnknownOption', `unknown option "-${flag}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
559
- }
560
- if (opt.type === 'boolean') {
561
- opts[opt.long] = true;
562
- continue;
563
- }
564
- if (j < flags.length - 1) {
565
- throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
566
- }
567
- if (idx + 1 < argv.length && !argv[idx + 1].startsWith('-')) {
568
- const value = argv[idx + 1];
569
- this.#applyValue(opt, value, opts);
570
- return idx + 2;
571
- }
572
- throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
573
- }
574
- return idx + 1;
575
- }
576
462
  #applyValue(opt, rawValue, opts) {
577
463
  const type = opt.type ?? 'string';
578
464
  let parsedValue = rawValue;
@@ -605,14 +491,14 @@ class Command {
605
491
  opts[opt.long] = parsedValue;
606
492
  }
607
493
  }
608
- #getMergedOptions() {
494
+ #getMergedOptions(includeVersion = !this.#parent) {
609
495
  const optionMap = new Map();
610
496
  const hasUserHelp = this.#options.some(o => o.long === 'help');
611
497
  const hasUserVersion = this.#options.some(o => o.long === 'version');
612
498
  if (!hasUserHelp) {
613
499
  optionMap.set('help', BUILTIN_HELP_OPTION);
614
500
  }
615
- if (!hasUserVersion) {
501
+ if (!hasUserVersion && includeVersion) {
616
502
  optionMap.set('version', BUILTIN_VERSION_OPTION);
617
503
  }
618
504
  for (const opt of this.#options) {
@@ -620,6 +506,26 @@ class Command {
620
506
  }
621
507
  return Array.from(optionMap.values());
622
508
  }
509
+ #validateMergedShortOptions(chain, includeRootVersion) {
510
+ const mergedByLong = new Map();
511
+ const rootCommand = chain[0];
512
+ for (const cmd of chain) {
513
+ const includeVersion = cmd === rootCommand && includeRootVersion;
514
+ for (const opt of cmd.#getMergedOptions(includeVersion)) {
515
+ mergedByLong.set(opt.long, opt);
516
+ }
517
+ }
518
+ const shortMap = new Map();
519
+ for (const opt of mergedByLong.values()) {
520
+ if (!opt.short)
521
+ continue;
522
+ const existingLong = shortMap.get(opt.short);
523
+ if (existingLong && existingLong !== opt.long) {
524
+ throw new CommanderError('OptionConflict', `short option "-${opt.short}" conflicts with "--${existingLong}"`, this.#getCommandPath());
525
+ }
526
+ shortMap.set(opt.short, opt.long);
527
+ }
528
+ }
623
529
  #validateOptionConfig(opt) {
624
530
  if (opt.long.startsWith('no-')) {
625
531
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no-": "${opt.long}"`, this.#getCommandPath());
@@ -640,6 +546,9 @@ class Command {
640
546
  }
641
547
  }
642
548
  #validateArgumentConfig(arg) {
549
+ if (arg.kind === 'required' && arg.default !== undefined) {
550
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
551
+ }
643
552
  if (arg.kind === 'variadic') {
644
553
  if (this.#arguments.some(a => a.kind === 'variadic')) {
645
554
  throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
@@ -658,6 +567,61 @@ class Command {
658
567
  }
659
568
  }
660
569
  }
570
+ #parseArguments(rawArgs) {
571
+ const argumentDefs = this.#arguments;
572
+ const args = {};
573
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
574
+ if (rawArgs.length < requiredCount) {
575
+ const missing = argumentDefs
576
+ .filter(a => a.kind === 'required')
577
+ .slice(rawArgs.length)
578
+ .map(a => a.name);
579
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
580
+ }
581
+ let index = 0;
582
+ for (const def of argumentDefs) {
583
+ if (def.kind === 'variadic') {
584
+ const rest = rawArgs.slice(index);
585
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
586
+ index = rawArgs.length;
587
+ break;
588
+ }
589
+ const raw = rawArgs[index];
590
+ if (raw === undefined) {
591
+ if (def.kind === 'optional') {
592
+ args[def.name] = def.default ?? undefined;
593
+ continue;
594
+ }
595
+ }
596
+ else {
597
+ args[def.name] = this.#convertArgument(def, raw);
598
+ index += 1;
599
+ }
600
+ }
601
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
602
+ if (!hasVariadic && index < rawArgs.length) {
603
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
604
+ }
605
+ return { args, rawArgs };
606
+ }
607
+ #convertArgument(def, raw) {
608
+ if (def.coerce) {
609
+ try {
610
+ return def.coerce(raw);
611
+ }
612
+ catch {
613
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
614
+ }
615
+ }
616
+ if (def.type === 'number') {
617
+ const n = Number(raw);
618
+ if (Number.isNaN(n)) {
619
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
620
+ }
621
+ return n;
622
+ }
623
+ return raw;
624
+ }
661
625
  #buildOptionMaps(allOptions, excludeResolver = false) {
662
626
  const optionByLong = new Map();
663
627
  const optionByShort = new Map();
@@ -752,7 +716,15 @@ class Command {
752
716
  return result;
753
717
  }
754
718
  #getCommandPath() {
755
- return this.#name;
719
+ const parts = [];
720
+ let current = this;
721
+ while (current) {
722
+ if (current.#name) {
723
+ parts.unshift(current.#name);
724
+ }
725
+ current = current.#parent;
726
+ }
727
+ return parts.join(' ') || this.#name;
756
728
  }
757
729
  #tryConsumeLongOption(tokens, idx, optionByLong, opts) {
758
730
  const token = tokens[idx];
@@ -46,14 +46,25 @@ interface IOption<T = unknown> {
46
46
  }
47
47
  /** Argument kind */
48
48
  type IArgumentKind = 'required' | 'optional' | 'variadic';
49
- /** Positional argument definition */
50
- interface IArgument {
49
+ /** Argument value type */
50
+ type IArgumentType = 'string' | 'number';
51
+ /**
52
+ * Positional argument definition.
53
+ * @template T - The type of the argument value
54
+ */
55
+ interface IArgument<T = unknown> {
51
56
  /** Argument name */
52
57
  name: string;
53
58
  /** Argument description */
54
59
  description: string;
55
60
  /** Argument kind: required / optional / variadic */
56
61
  kind: IArgumentKind;
62
+ /** Value type, defaults to 'string' */
63
+ type?: IArgumentType;
64
+ /** Default value when not provided (only effective for optional arguments) */
65
+ default?: T;
66
+ /** Custom value transformation (takes precedence over type conversion) */
67
+ coerce?: (rawValue: string) => T;
57
68
  }
58
69
  /** Command configuration */
59
70
  interface ICommandConfig {
@@ -61,16 +72,19 @@ interface ICommandConfig {
61
72
  name?: string;
62
73
  /** Command description */
63
74
  description: string;
64
- /** Version (only effective for root command) */
75
+ /** Version (only effective for built-in root --version) */
65
76
  version?: string;
66
77
  /** Enable built-in "help" subcommand (only effective when command has subcommands) */
67
78
  help?: boolean;
79
+ /** Default reporter for this command (can be overridden by run params) */
80
+ reporter?: IReporter;
68
81
  }
69
82
  /** Forward declaration for Command class */
70
83
  interface ICommand {
71
84
  readonly name: string;
72
85
  readonly description: string;
73
86
  readonly version: string | undefined;
87
+ readonly parent?: ICommand;
74
88
  readonly options: IOption[];
75
89
  readonly arguments: IArgument[];
76
90
  }
@@ -91,8 +105,10 @@ interface IActionParams {
91
105
  ctx: ICommandContext;
92
106
  /** Parsed options */
93
107
  opts: Record<string, unknown>;
94
- /** Parsed positional arguments */
95
- args: string[];
108
+ /** Parsed positional arguments (keyed by argument name) */
109
+ args: Record<string, unknown>;
110
+ /** Raw positional argument strings (before type conversion) */
111
+ rawArgs: string[];
96
112
  }
97
113
  /** Action handler function */
98
114
  type IAction = (params: IActionParams) => void | Promise<void>;
@@ -102,15 +118,17 @@ interface IRunParams {
102
118
  argv: string[];
103
119
  /** Environment variables (usually process.env) */
104
120
  envs: Record<string, string | undefined>;
105
- /** Optional reporter for logging (defaults to console reporter) */
121
+ /** Optional reporter override (defaults to command's reporter or console reporter) */
106
122
  reporter?: IReporter;
107
123
  }
108
124
  /** parse() method result */
109
125
  interface IParseResult {
110
126
  /** Parsed options */
111
127
  opts: Record<string, unknown>;
112
- /** Parsed positional arguments */
113
- args: string[];
128
+ /** Parsed positional arguments (keyed by argument name) */
129
+ args: Record<string, unknown>;
130
+ /** Raw positional argument strings (before type conversion) */
131
+ rawArgs: string[];
114
132
  }
115
133
  /** shift() method result */
116
134
  interface IShiftResult {
@@ -120,7 +138,7 @@ interface IShiftResult {
120
138
  remaining: string[];
121
139
  }
122
140
  /** Error kinds for command parsing */
123
- type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
141
+ type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
124
142
  /** Commander error with structured information */
125
143
  declare class CommanderError extends Error {
126
144
  readonly kind: ICommanderErrorKind;
@@ -176,6 +194,7 @@ declare class Command implements ICommand {
176
194
  get name(): string;
177
195
  get description(): string;
178
196
  get version(): string | undefined;
197
+ get parent(): Command | undefined;
179
198
  get options(): IOption[];
180
199
  get arguments(): IArgument[];
181
200
  option(opt: IOption): this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",