@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 +10 -0
- package/lib/cjs/index.cjs +65 -16
- package/lib/esm/index.mjs +65 -16
- package/lib/types/index.d.ts +22 -7
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
249
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
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();
|
package/lib/types/index.d.ts
CHANGED
|
@@ -46,14 +46,25 @@ interface IOption<T = unknown> {
|
|
|
46
46
|
}
|
|
47
47
|
/** Argument kind */
|
|
48
48
|
type IArgumentKind = 'required' | 'optional' | 'variadic';
|
|
49
|
-
/**
|
|
50
|
-
|
|
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;
|