@guanghechen/commander 3.1.0 → 3.2.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,15 @@
1
1
  # Change Log
2
2
 
3
+ ## 3.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Change args from string[] to Record<string, unknown> with type/coerce/default support:
8
+ - `args` is now `Record<string, unknown>` keyed by argument name
9
+ - Add `rawArgs: string[]` for original argument strings before type conversion
10
+ - IArgument now supports `type`, `default`, and `coerce` properties
11
+ - Add `TooManyArguments` error kind for extra arguments validation
12
+
3
13
  ## 3.1.0
4
14
 
5
15
  ### Minor Changes
package/lib/cjs/index.cjs CHANGED
@@ -149,13 +149,8 @@ class Command {
149
149
  };
150
150
  this.#applyChain(chain, optsMap, ctx);
151
151
  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 };
152
+ const { args, rawArgs } = leafCommand.#parseArguments(restArgs);
153
+ const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
159
154
  if (leafCommand.#action) {
160
155
  try {
161
156
  await leafCommand.#action(actionParams);
@@ -189,7 +184,7 @@ class Command {
189
184
  parse(argv) {
190
185
  const allOptions = this.#getMergedOptions();
191
186
  const opts = {};
192
- const args = [];
187
+ const rawArgs = [];
193
188
  for (const opt of allOptions) {
194
189
  if (opt.default !== undefined) {
195
190
  opts[opt.long] = opt.default;
@@ -214,7 +209,7 @@ class Command {
214
209
  while (i < remaining.length) {
215
210
  const token = remaining[i];
216
211
  if (token === '--') {
217
- args.push(...remaining.slice(i + 1));
212
+ rawArgs.push(...remaining.slice(i + 1));
218
213
  break;
219
214
  }
220
215
  if (token.startsWith('--')) {
@@ -225,7 +220,7 @@ class Command {
225
220
  i = this.#parseShortOption(remaining, i, optionByShort, opts);
226
221
  continue;
227
222
  }
228
- args.push(token);
223
+ rawArgs.push(token);
229
224
  i += 1;
230
225
  }
231
226
  for (const opt of allOptions) {
@@ -245,12 +240,8 @@ class Command {
245
240
  }
246
241
  }
247
242
  }
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 };
243
+ const { args } = this.#parseArguments(rawArgs);
244
+ return { opts, args, rawArgs };
254
245
  }
255
246
  shift(tokens) {
256
247
  return this.#shiftWithShadowed(tokens, new Set());
@@ -662,6 +653,9 @@ class Command {
662
653
  }
663
654
  }
664
655
  #validateArgumentConfig(arg) {
656
+ if (arg.kind === 'required' && arg.default !== undefined) {
657
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
658
+ }
665
659
  if (arg.kind === 'variadic') {
666
660
  if (this.#arguments.some(a => a.kind === 'variadic')) {
667
661
  throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
@@ -680,6 +674,61 @@ class Command {
680
674
  }
681
675
  }
682
676
  }
677
+ #parseArguments(rawArgs) {
678
+ const argumentDefs = this.#arguments;
679
+ const args = {};
680
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
681
+ if (rawArgs.length < requiredCount) {
682
+ const missing = argumentDefs
683
+ .filter(a => a.kind === 'required')
684
+ .slice(rawArgs.length)
685
+ .map(a => a.name);
686
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
687
+ }
688
+ let index = 0;
689
+ for (const def of argumentDefs) {
690
+ if (def.kind === 'variadic') {
691
+ const rest = rawArgs.slice(index);
692
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
693
+ index = rawArgs.length;
694
+ break;
695
+ }
696
+ const raw = rawArgs[index];
697
+ if (raw === undefined) {
698
+ if (def.kind === 'optional') {
699
+ args[def.name] = def.default ?? undefined;
700
+ continue;
701
+ }
702
+ }
703
+ else {
704
+ args[def.name] = this.#convertArgument(def, raw);
705
+ index += 1;
706
+ }
707
+ }
708
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
709
+ if (!hasVariadic && index < rawArgs.length) {
710
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
711
+ }
712
+ return { args, rawArgs };
713
+ }
714
+ #convertArgument(def, raw) {
715
+ if (def.coerce) {
716
+ try {
717
+ return def.coerce(raw);
718
+ }
719
+ catch {
720
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
721
+ }
722
+ }
723
+ if (def.type === 'number') {
724
+ const n = Number(raw);
725
+ if (Number.isNaN(n)) {
726
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
727
+ }
728
+ return n;
729
+ }
730
+ return raw;
731
+ }
683
732
  #buildOptionMaps(allOptions, excludeResolver = false) {
684
733
  const optionByLong = new Map();
685
734
  const optionByShort = new Map();
package/lib/esm/index.mjs CHANGED
@@ -127,13 +127,8 @@ class Command {
127
127
  };
128
128
  this.#applyChain(chain, optsMap, ctx);
129
129
  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 };
130
+ const { args, rawArgs } = leafCommand.#parseArguments(restArgs);
131
+ const actionParams = { ctx, opts: mergedOpts, args, rawArgs };
137
132
  if (leafCommand.#action) {
138
133
  try {
139
134
  await leafCommand.#action(actionParams);
@@ -167,7 +162,7 @@ class Command {
167
162
  parse(argv) {
168
163
  const allOptions = this.#getMergedOptions();
169
164
  const opts = {};
170
- const args = [];
165
+ const rawArgs = [];
171
166
  for (const opt of allOptions) {
172
167
  if (opt.default !== undefined) {
173
168
  opts[opt.long] = opt.default;
@@ -192,7 +187,7 @@ class Command {
192
187
  while (i < remaining.length) {
193
188
  const token = remaining[i];
194
189
  if (token === '--') {
195
- args.push(...remaining.slice(i + 1));
190
+ rawArgs.push(...remaining.slice(i + 1));
196
191
  break;
197
192
  }
198
193
  if (token.startsWith('--')) {
@@ -203,7 +198,7 @@ class Command {
203
198
  i = this.#parseShortOption(remaining, i, optionByShort, opts);
204
199
  continue;
205
200
  }
206
- args.push(token);
201
+ rawArgs.push(token);
207
202
  i += 1;
208
203
  }
209
204
  for (const opt of allOptions) {
@@ -223,12 +218,8 @@ class Command {
223
218
  }
224
219
  }
225
220
  }
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 };
221
+ const { args } = this.#parseArguments(rawArgs);
222
+ return { opts, args, rawArgs };
232
223
  }
233
224
  shift(tokens) {
234
225
  return this.#shiftWithShadowed(tokens, new Set());
@@ -640,6 +631,9 @@ class Command {
640
631
  }
641
632
  }
642
633
  #validateArgumentConfig(arg) {
634
+ if (arg.kind === 'required' && arg.default !== undefined) {
635
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
636
+ }
643
637
  if (arg.kind === 'variadic') {
644
638
  if (this.#arguments.some(a => a.kind === 'variadic')) {
645
639
  throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
@@ -658,6 +652,61 @@ class Command {
658
652
  }
659
653
  }
660
654
  }
655
+ #parseArguments(rawArgs) {
656
+ const argumentDefs = this.#arguments;
657
+ const args = {};
658
+ const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
659
+ if (rawArgs.length < requiredCount) {
660
+ const missing = argumentDefs
661
+ .filter(a => a.kind === 'required')
662
+ .slice(rawArgs.length)
663
+ .map(a => a.name);
664
+ throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
665
+ }
666
+ let index = 0;
667
+ for (const def of argumentDefs) {
668
+ if (def.kind === 'variadic') {
669
+ const rest = rawArgs.slice(index);
670
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
671
+ index = rawArgs.length;
672
+ break;
673
+ }
674
+ const raw = rawArgs[index];
675
+ if (raw === undefined) {
676
+ if (def.kind === 'optional') {
677
+ args[def.name] = def.default ?? undefined;
678
+ continue;
679
+ }
680
+ }
681
+ else {
682
+ args[def.name] = this.#convertArgument(def, raw);
683
+ index += 1;
684
+ }
685
+ }
686
+ const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
687
+ if (!hasVariadic && index < rawArgs.length) {
688
+ throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
689
+ }
690
+ return { args, rawArgs };
691
+ }
692
+ #convertArgument(def, raw) {
693
+ if (def.coerce) {
694
+ try {
695
+ return def.coerce(raw);
696
+ }
697
+ catch {
698
+ throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
699
+ }
700
+ }
701
+ if (def.type === 'number') {
702
+ const n = Number(raw);
703
+ if (Number.isNaN(n)) {
704
+ throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
705
+ }
706
+ return n;
707
+ }
708
+ return raw;
709
+ }
661
710
  #buildOptionMaps(allOptions, excludeResolver = false) {
662
711
  const optionByLong = new Map();
663
712
  const optionByShort = new Map();
@@ -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 {
@@ -91,8 +102,10 @@ interface IActionParams {
91
102
  ctx: ICommandContext;
92
103
  /** Parsed options */
93
104
  opts: Record<string, unknown>;
94
- /** Parsed positional arguments */
95
- args: string[];
105
+ /** Parsed positional arguments (keyed by argument name) */
106
+ args: Record<string, unknown>;
107
+ /** Raw positional argument strings (before type conversion) */
108
+ rawArgs: string[];
96
109
  }
97
110
  /** Action handler function */
98
111
  type IAction = (params: IActionParams) => void | Promise<void>;
@@ -109,8 +122,10 @@ interface IRunParams {
109
122
  interface IParseResult {
110
123
  /** Parsed options */
111
124
  opts: Record<string, unknown>;
112
- /** Parsed positional arguments */
113
- args: string[];
125
+ /** Parsed positional arguments (keyed by argument name) */
126
+ args: Record<string, unknown>;
127
+ /** Raw positional argument strings (before type conversion) */
128
+ rawArgs: string[];
114
129
  }
115
130
  /** shift() method result */
116
131
  interface IShiftResult {
@@ -120,7 +135,7 @@ interface IShiftResult {
120
135
  remaining: string[];
121
136
  }
122
137
  /** Error kinds for command parsing */
123
- type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
138
+ type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'TooManyArguments' | 'ConfigurationError';
124
139
  /** Commander error with structured information */
125
140
  declare class CommanderError extends Error {
126
141
  readonly kind: ICommanderErrorKind;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanghechen/commander",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "A minimal, type-safe command-line interface builder with fluent API",
5
5
  "author": {
6
6
  "name": "guanghechen",