clap-ts 0.1.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/LICENSE +21 -0
- package/README.md +752 -0
- package/dist/help.d.ts +31 -0
- package/dist/help.js +414 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +646 -0
- package/dist/runner.d.ts +60 -0
- package/dist/runner.js +370 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +5 -0
- package/dist/validation.d.ts +12 -0
- package/dist/validation.js +352 -0
- package/package.json +57 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parser - delegates core tokenizing to node:util parseArgs,
|
|
3
|
+
* then layers on: env fallback, type coercion, count/append actions,
|
|
4
|
+
* numArgs with defaultMissingValue, global args, kebab-to-camel mapping,
|
|
5
|
+
* subcommand detection, and default values.
|
|
6
|
+
*
|
|
7
|
+
* node:util parseArgs handles:
|
|
8
|
+
* --flag, --flag=value, --flag value, -f, -fvalue, -abc (combined booleans),
|
|
9
|
+
* -- separator, positionals
|
|
10
|
+
*
|
|
11
|
+
* We handle on top:
|
|
12
|
+
* conflictsWith / requires (in validation.ts),
|
|
13
|
+
* env variable fallback, action: 'append' (via multiple:true), action: 'count',
|
|
14
|
+
* numArgs with defaultMissingValue, global args merge, valueParser enum validation
|
|
15
|
+
* (in validation.ts), number type coercion, kebab-to-camel mapping,
|
|
16
|
+
* required arg validation (in validation.ts), typo suggestions (in validation.ts),
|
|
17
|
+
* valueDelimiter splitting, function valueParser, trailingVarArg, last,
|
|
18
|
+
* allowHyphenValues, allowNegativeNumbers, inferLongArgs, defaultValueIf.
|
|
19
|
+
*/
|
|
20
|
+
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
21
|
+
// ---- Helpers ----
|
|
22
|
+
/** Convert kebab-case to camelCase: --config-path -> configPath */
|
|
23
|
+
function kebabToCamel(s) {
|
|
24
|
+
return s.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
/** Get the raw argv slice (after the binary/script path). */
|
|
27
|
+
export function getRawArgs(argv) {
|
|
28
|
+
if (argv) {
|
|
29
|
+
return [...argv];
|
|
30
|
+
}
|
|
31
|
+
// Bun.argv includes [bun, script, ...args], same as process.argv
|
|
32
|
+
const source = globalThis.Bun === undefined ? process.argv : globalThis.Bun.argv;
|
|
33
|
+
return source.slice(2);
|
|
34
|
+
}
|
|
35
|
+
// ---- Error ----
|
|
36
|
+
export class CliParseError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'CliParseError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ---- Flag Lookup Maps ----
|
|
43
|
+
/**
|
|
44
|
+
* Build lookup maps from all possible flag forms to the canonical arg name.
|
|
45
|
+
* Used for post-processing parseArgs output back to our ArgDef keys.
|
|
46
|
+
* Registers both hidden aliases and visible aliases.
|
|
47
|
+
*/
|
|
48
|
+
function buildFlagMaps(argsDef) {
|
|
49
|
+
const longMap = new Map();
|
|
50
|
+
const shortMap = new Map();
|
|
51
|
+
let hasHyphenOrNegative = false;
|
|
52
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
53
|
+
const longName = def.long ?? key;
|
|
54
|
+
longMap.set(longName, { key, def, negated: false });
|
|
55
|
+
if (def.type === 'boolean') {
|
|
56
|
+
longMap.set(`no-${longName}`, { key, def, negated: true });
|
|
57
|
+
}
|
|
58
|
+
if (def.allowHyphenValues || def.allowNegativeNumbers) {
|
|
59
|
+
hasHyphenOrNegative = true;
|
|
60
|
+
}
|
|
61
|
+
// Register hidden aliases
|
|
62
|
+
if (def.alias) {
|
|
63
|
+
for (const alias of def.alias) {
|
|
64
|
+
if (alias.length === 1) {
|
|
65
|
+
shortMap.set(alias, { key, def });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
longMap.set(alias, { key, def, negated: false });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Register visible aliases (also work as real aliases at parse time)
|
|
73
|
+
if (def.visibleAlias) {
|
|
74
|
+
for (const alias of def.visibleAlias) {
|
|
75
|
+
if (alias.length === 1) {
|
|
76
|
+
shortMap.set(alias, { key, def });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
longMap.set(alias, { key, def, negated: false });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (def.short && def.short.length === 1) {
|
|
84
|
+
shortMap.set(def.short, { key, def });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { longMap, shortMap, hasHyphenOrNegative };
|
|
88
|
+
}
|
|
89
|
+
// ---- Build parseArgs options config ----
|
|
90
|
+
/**
|
|
91
|
+
* Build the `options` config that node:util parseArgs expects from our ArgDef definitions.
|
|
92
|
+
*/
|
|
93
|
+
function buildParseArgsOptions(argsDef) {
|
|
94
|
+
const options = {};
|
|
95
|
+
const optionalValueFlags = new Set();
|
|
96
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
97
|
+
if (def.type === 'positional') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const longName = def.long ?? key;
|
|
101
|
+
if (def.action === 'count') {
|
|
102
|
+
options[longName] = { type: 'boolean', multiple: true };
|
|
103
|
+
}
|
|
104
|
+
else if (def.type === 'boolean') {
|
|
105
|
+
options[longName] = { type: 'boolean' };
|
|
106
|
+
options[`no-${longName}`] = { type: 'boolean' };
|
|
107
|
+
}
|
|
108
|
+
else if (def.action === 'append') {
|
|
109
|
+
options[longName] = { type: 'string', multiple: true };
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
options[longName] = { type: 'string' };
|
|
113
|
+
}
|
|
114
|
+
if (def.numArgs && def.numArgs.min === 0 && def.type !== 'boolean') {
|
|
115
|
+
optionalValueFlags.add(longName);
|
|
116
|
+
}
|
|
117
|
+
if (def.short && def.short.length === 1) {
|
|
118
|
+
options[longName].short = def.short;
|
|
119
|
+
}
|
|
120
|
+
// Register hidden and visible aliases
|
|
121
|
+
const registerAliases = (aliases) => {
|
|
122
|
+
const opt = options[longName];
|
|
123
|
+
for (const alias of aliases) {
|
|
124
|
+
if (alias.length > 1 || opt.short) {
|
|
125
|
+
options[alias] = { type: opt.type, multiple: opt.multiple };
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
opt.short = alias;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (def.alias) {
|
|
133
|
+
registerAliases(def.alias);
|
|
134
|
+
}
|
|
135
|
+
if (def.visibleAlias) {
|
|
136
|
+
registerAliases(def.visibleAlias);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Built-in flags
|
|
140
|
+
options['help'] = { type: 'boolean', short: 'h' };
|
|
141
|
+
options['version'] = { type: 'boolean', short: 'V' };
|
|
142
|
+
return { options, optionalValueFlags };
|
|
143
|
+
}
|
|
144
|
+
// ---- Pre-scan / Preprocessing ----
|
|
145
|
+
/** Check if the next token looks like a flag (not a value). */
|
|
146
|
+
function isNextTokenAFlag(nextToken) {
|
|
147
|
+
return (nextToken === undefined ||
|
|
148
|
+
(nextToken.startsWith('-') && nextToken !== '-' && nextToken !== '--'));
|
|
149
|
+
}
|
|
150
|
+
/** Check if a token looks like a negative number (e.g., -1, -3.14, -0). */
|
|
151
|
+
function looksLikeNegativeNumber(token) {
|
|
152
|
+
return /^-\d/.test(token);
|
|
153
|
+
}
|
|
154
|
+
/** Try to match a long flag token as an optional-value flag without a value. */
|
|
155
|
+
function tryStripOptionalLong(token, nextToken, optionalValueFlags, longMap) {
|
|
156
|
+
if (!token.startsWith('--') || token.length <= 2 || token.includes('=')) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const flagName = token.slice(2);
|
|
160
|
+
if (!optionalValueFlags.has(flagName) || !isNextTokenAFlag(nextToken)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const entry = longMap.get(flagName);
|
|
164
|
+
if (!entry) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return { key: entry.key, defaultVal: entry.def.defaultMissingValue ?? true };
|
|
168
|
+
}
|
|
169
|
+
/** Try to match a short flag token as an optional-value flag without a value. */
|
|
170
|
+
function tryStripOptionalShort(token, nextToken, optionalValueFlags, shortMap) {
|
|
171
|
+
if (!token.startsWith('-') || token.length !== 2 || token[1] === '-') {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const c = token[1];
|
|
175
|
+
const entry = shortMap.get(c);
|
|
176
|
+
if (!entry) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const entryLong = entry.def.long ?? entry.key;
|
|
180
|
+
if (!optionalValueFlags.has(entryLong) || !isNextTokenAFlag(nextToken)) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
return { key: entry.key, defaultVal: entry.def.defaultMissingValue ?? true };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a flag token to its key and ArgDef, checking both long and short maps.
|
|
187
|
+
* Returns the canonical key (used as fallback long name) and the def.
|
|
188
|
+
*/
|
|
189
|
+
function resolveFlag(token, longMap, shortMap) {
|
|
190
|
+
if (token.startsWith('--') && token.length > 2) {
|
|
191
|
+
const flagName = token.includes('=') ? token.slice(2, token.indexOf('=')) : token.slice(2);
|
|
192
|
+
const entry = longMap.get(flagName);
|
|
193
|
+
return entry ? { key: entry.key, def: entry.def } : undefined;
|
|
194
|
+
}
|
|
195
|
+
if (token.startsWith('-') && token.length === 2 && token[1] !== '-') {
|
|
196
|
+
return shortMap.get(token[1]) ?? undefined;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Pre-process raw args before passing to node:util parseArgs.
|
|
202
|
+
* Handles:
|
|
203
|
+
* 1. Optional-value flags (strip bare flags, record defaults)
|
|
204
|
+
* 2. allowHyphenValues / allowNegativeNumbers (rewrite --flag -value to --flag=-value)
|
|
205
|
+
*/
|
|
206
|
+
function preprocessArgs(rawArgs, optionalValueFlags, longMap, shortMap, hasHyphenOrNegative) {
|
|
207
|
+
if (optionalValueFlags.size === 0 && !hasHyphenOrNegative) {
|
|
208
|
+
return { processedArgs: [...rawArgs], optionalDefaults: new Map() };
|
|
209
|
+
}
|
|
210
|
+
const optionalDefaults = new Map();
|
|
211
|
+
const processedArgs = [];
|
|
212
|
+
let stopParsing = false;
|
|
213
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
214
|
+
const token = rawArgs[i];
|
|
215
|
+
if (stopParsing || token === '--') {
|
|
216
|
+
if (token === '--') {
|
|
217
|
+
stopParsing = true;
|
|
218
|
+
}
|
|
219
|
+
processedArgs.push(token);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const nextToken = rawArgs[i + 1];
|
|
223
|
+
// Optional-value flag handling (long)
|
|
224
|
+
const stripped = tryStripOptionalLong(token, nextToken, optionalValueFlags, longMap);
|
|
225
|
+
if (stripped) {
|
|
226
|
+
optionalDefaults.set(stripped.key, stripped.defaultVal);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Optional-value flag handling (short)
|
|
230
|
+
const strippedShort = tryStripOptionalShort(token, nextToken, optionalValueFlags, shortMap);
|
|
231
|
+
if (strippedShort) {
|
|
232
|
+
optionalDefaults.set(strippedShort.key, strippedShort.defaultVal);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// allowHyphenValues / allowNegativeNumbers:
|
|
236
|
+
// If this token is a known flag and the next token starts with - but the flag's
|
|
237
|
+
// def allows hyphen values or negative numbers, rewrite to --flag=-value
|
|
238
|
+
if (nextToken && nextToken.startsWith('-') && nextToken !== '-' && nextToken !== '--') {
|
|
239
|
+
const resolved = resolveFlag(token, longMap, shortMap);
|
|
240
|
+
if (resolved && resolved.def.type !== 'boolean' && resolved.def.type !== 'positional') {
|
|
241
|
+
const shouldRewrite = resolved.def.allowHyphenValues ||
|
|
242
|
+
(resolved.def.allowNegativeNumbers && looksLikeNegativeNumber(nextToken));
|
|
243
|
+
if (shouldRewrite && !token.includes('=')) {
|
|
244
|
+
// Rewrite to --longName=value form so node:util parseArgs treats it as a value
|
|
245
|
+
const longName = resolved.def.long ?? resolved.key;
|
|
246
|
+
processedArgs.push(`--${longName}=${nextToken}`);
|
|
247
|
+
i++; // skip the next token (consumed as value)
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
processedArgs.push(token);
|
|
253
|
+
}
|
|
254
|
+
return { processedArgs, optionalDefaults };
|
|
255
|
+
}
|
|
256
|
+
// ---- Coerce Value ----
|
|
257
|
+
function coerceValue(value, def, argName) {
|
|
258
|
+
// If a function valueParser is defined, use it for coercion
|
|
259
|
+
if (typeof def.valueParser === 'function') {
|
|
260
|
+
try {
|
|
261
|
+
const parsed = def.valueParser(value);
|
|
262
|
+
// Ensure the result is a valid ParseResult value type
|
|
263
|
+
if (typeof parsed === 'string' ||
|
|
264
|
+
typeof parsed === 'number' ||
|
|
265
|
+
typeof parsed === 'boolean') {
|
|
266
|
+
return parsed;
|
|
267
|
+
}
|
|
268
|
+
return String(parsed);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
272
|
+
throw new CliParseError(`invalid value '${value}' for '${argName}': ${msg}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
switch (def.type) {
|
|
276
|
+
case 'boolean': {
|
|
277
|
+
const lower = value.toLowerCase();
|
|
278
|
+
return lower === 'true' || lower === '1' || lower === 'yes';
|
|
279
|
+
}
|
|
280
|
+
case 'number': {
|
|
281
|
+
const num = value.includes('.') ? Number.parseFloat(value) : Number.parseInt(value, 10);
|
|
282
|
+
if (Number.isNaN(num) || !Number.isFinite(num)) {
|
|
283
|
+
throw new CliParseError(`invalid value '${value}' for '${argName}': expected a finite number`);
|
|
284
|
+
}
|
|
285
|
+
return num;
|
|
286
|
+
}
|
|
287
|
+
case 'enum':
|
|
288
|
+
case 'string':
|
|
289
|
+
case 'positional': {
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ---- Subcommand Detection ----
|
|
295
|
+
/**
|
|
296
|
+
* Build a lookup map from subcommand names and aliases to canonical names.
|
|
297
|
+
* Used for O(1) subcommand detection.
|
|
298
|
+
*/
|
|
299
|
+
function buildSubcommandMap(subCommands) {
|
|
300
|
+
const map = new Map();
|
|
301
|
+
for (const [name, def] of Object.entries(subCommands)) {
|
|
302
|
+
map.set(name, name);
|
|
303
|
+
if (def.meta.aliases) {
|
|
304
|
+
for (const alias of def.meta.aliases) {
|
|
305
|
+
map.set(alias, name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return map;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Scan positionals for a subcommand name or alias using O(1) map lookup.
|
|
313
|
+
* Returns the canonical name and the index in the positionals array.
|
|
314
|
+
*/
|
|
315
|
+
function detectSubcommand(positionals, command) {
|
|
316
|
+
if (!command.subCommands) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
const subMap = buildSubcommandMap(command.subCommands);
|
|
320
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
321
|
+
const canonical = subMap.get(positionals[i]);
|
|
322
|
+
if (canonical) {
|
|
323
|
+
return { name: canonical, index: i };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
// ---- Separate Rest Args ----
|
|
329
|
+
/**
|
|
330
|
+
* parseArgs lumps everything (before and after --) into its `positionals` array.
|
|
331
|
+
* We need to split them into positionals (before --) and rest (after --).
|
|
332
|
+
*/
|
|
333
|
+
function splitPositionalsAndRest(rawArgs, parseArgsPositionals) {
|
|
334
|
+
let dashDashIdx = -1;
|
|
335
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
336
|
+
if (rawArgs[i] === '--') {
|
|
337
|
+
dashDashIdx = i;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (dashDashIdx === -1) {
|
|
342
|
+
return { positionals: [...parseArgsPositionals], rest: [] };
|
|
343
|
+
}
|
|
344
|
+
const restCount = rawArgs.length - dashDashIdx - 1;
|
|
345
|
+
const totalPositionals = parseArgsPositionals.length;
|
|
346
|
+
const beforeCount = totalPositionals - restCount;
|
|
347
|
+
return {
|
|
348
|
+
positionals: parseArgsPositionals.slice(0, Math.max(0, beforeCount)),
|
|
349
|
+
rest: parseArgsPositionals.slice(Math.max(0, beforeCount)),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// ---- Infer Long Args ----
|
|
353
|
+
/**
|
|
354
|
+
* Try to resolve an unknown long flag by prefix matching.
|
|
355
|
+
* Returns the matched entry if exactly one match, undefined otherwise.
|
|
356
|
+
*/
|
|
357
|
+
function tryInferLongArg(flagName, longMap) {
|
|
358
|
+
const matches = [];
|
|
359
|
+
for (const [name, entry] of longMap) {
|
|
360
|
+
if (name.startsWith(flagName) && !entry.negated) {
|
|
361
|
+
matches.push(entry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
365
|
+
}
|
|
366
|
+
// ---- Value Delimiter Splitting ----
|
|
367
|
+
/**
|
|
368
|
+
* Split a string value by the delimiter and return as array.
|
|
369
|
+
* Applied after initial parsing for args with valueDelimiter.
|
|
370
|
+
*/
|
|
371
|
+
function splitByDelimiter(value, delimiter) {
|
|
372
|
+
if (Array.isArray(value)) {
|
|
373
|
+
const result = [];
|
|
374
|
+
for (const v of value) {
|
|
375
|
+
result.push(...v.split(delimiter));
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
return value.split(delimiter);
|
|
380
|
+
}
|
|
381
|
+
// ---- Main Parser ----
|
|
382
|
+
/**
|
|
383
|
+
* Parse raw argument tokens against a command definition.
|
|
384
|
+
*
|
|
385
|
+
* Uses node:util parseArgs for core tokenizing, then layers on all clap-ts features.
|
|
386
|
+
*/
|
|
387
|
+
export function parseArgs(rawArgs, command) {
|
|
388
|
+
const argsDef = command.args ?? {};
|
|
389
|
+
const inferLong = command.meta.inferLongArgs === true;
|
|
390
|
+
// Build parseArgs config from our ArgDef definitions
|
|
391
|
+
const { options, optionalValueFlags } = buildParseArgsOptions(argsDef);
|
|
392
|
+
// Build flag maps once and reuse throughout parsing
|
|
393
|
+
const { longMap, shortMap, hasHyphenOrNegative } = buildFlagMaps(argsDef);
|
|
394
|
+
// Pre-process: handle optional-value flags, allowHyphenValues, allowNegativeNumbers
|
|
395
|
+
const { processedArgs, optionalDefaults } = preprocessArgs(rawArgs, optionalValueFlags, longMap, shortMap, hasHyphenOrNegative);
|
|
396
|
+
// Run node:util parseArgs (strict: false so unknown flags don't throw)
|
|
397
|
+
let parsed;
|
|
398
|
+
try {
|
|
399
|
+
parsed = nodeParseArgs({
|
|
400
|
+
args: processedArgs,
|
|
401
|
+
options,
|
|
402
|
+
allowPositionals: true,
|
|
403
|
+
strict: false,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
408
|
+
throw new CliParseError(msg);
|
|
409
|
+
}
|
|
410
|
+
const { values, positionals: rawPositionals } = parsed;
|
|
411
|
+
const result = {};
|
|
412
|
+
const unknown = [];
|
|
413
|
+
const explicitlySet = new Set();
|
|
414
|
+
// Extract help/version and detect short vs long help
|
|
415
|
+
const helpRequested = values['help'] === true;
|
|
416
|
+
const helpIsShort = helpRequested && rawArgs.some((a) => a === '-h');
|
|
417
|
+
const versionRequested = values['version'] === true;
|
|
418
|
+
// Process each parsed value back through our ArgDef system
|
|
419
|
+
for (const [parsedKey, rawValue] of Object.entries(values)) {
|
|
420
|
+
if (parsedKey === 'help' || parsedKey === 'version') {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Handle --no-<flag> negation
|
|
424
|
+
if (parsedKey.startsWith('no-') && rawValue === true) {
|
|
425
|
+
const positiveName = parsedKey.slice(3);
|
|
426
|
+
const entry = longMap.get(positiveName);
|
|
427
|
+
if (entry && entry.def.type === 'boolean') {
|
|
428
|
+
result[entry.key] = false;
|
|
429
|
+
explicitlySet.add(entry.key);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Look up canonical key via long map
|
|
434
|
+
const entry = longMap.get(parsedKey);
|
|
435
|
+
if (entry) {
|
|
436
|
+
applyValue(entry.key, entry.def, rawValue, result, explicitlySet);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
// Check short map
|
|
440
|
+
const shortEntry = shortMap.get(parsedKey);
|
|
441
|
+
if (shortEntry) {
|
|
442
|
+
applyValue(shortEntry.key, shortEntry.def, rawValue, result, explicitlySet);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// inferLongArgs: try prefix matching on unknown long flags
|
|
446
|
+
if (inferLong) {
|
|
447
|
+
const inferred = tryInferLongArg(parsedKey, longMap);
|
|
448
|
+
if (inferred) {
|
|
449
|
+
applyValue(inferred.key, inferred.def, rawValue, result, explicitlySet);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
unknown.push(`--${parsedKey}`);
|
|
454
|
+
}
|
|
455
|
+
// Apply optional-value defaults from pre-scan
|
|
456
|
+
for (const [key, defaultVal] of optionalDefaults) {
|
|
457
|
+
if (!explicitlySet.has(key)) {
|
|
458
|
+
result[key] = defaultVal;
|
|
459
|
+
explicitlySet.add(key);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Split positionals from rest (tokens after --)
|
|
463
|
+
const { positionals: allPositionals, rest } = splitPositionalsAndRest(rawArgs, rawPositionals);
|
|
464
|
+
// Detect subcommand in positionals
|
|
465
|
+
const subCmdResult = detectSubcommand(allPositionals, command);
|
|
466
|
+
const subCommand = subCmdResult?.name;
|
|
467
|
+
// Remove subcommand token from positionals
|
|
468
|
+
const positionals = [];
|
|
469
|
+
for (let i = 0; i < allPositionals.length; i++) {
|
|
470
|
+
if (subCmdResult && i === subCmdResult.index) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
positionals.push(allPositionals[i]);
|
|
474
|
+
}
|
|
475
|
+
// Assign positionals to positional arg defs
|
|
476
|
+
const positionalDefs = [];
|
|
477
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
478
|
+
if (def.type === 'positional') {
|
|
479
|
+
positionalDefs.push({ key, def });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (let p = 0; p < positionalDefs.length; p++) {
|
|
483
|
+
const { key, def } = positionalDefs[p];
|
|
484
|
+
// last: this positional only gets values from rest (after --)
|
|
485
|
+
if (def.last) {
|
|
486
|
+
if (rest.length > 0) {
|
|
487
|
+
result[key] = rest[0];
|
|
488
|
+
explicitlySet.add(key);
|
|
489
|
+
}
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// trailingVarArg: consume all remaining positionals as an array
|
|
493
|
+
if (def.trailingVarArg) {
|
|
494
|
+
if (p < positionals.length) {
|
|
495
|
+
result[key] = positionals.slice(p);
|
|
496
|
+
explicitlySet.add(key);
|
|
497
|
+
}
|
|
498
|
+
break; // no more positional defs to process
|
|
499
|
+
}
|
|
500
|
+
// Normal positional assignment
|
|
501
|
+
if (p < positionals.length) {
|
|
502
|
+
result[key] = coerceValue(positionals[p], def, key);
|
|
503
|
+
explicitlySet.add(key);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Single pass: valueDelimiter splitting, env fallback, conditional defaults, static defaults
|
|
507
|
+
const argsDefEntries = Object.entries(argsDef);
|
|
508
|
+
for (let a = 0; a < argsDefEntries.length; a++) {
|
|
509
|
+
const [key, def] = argsDefEntries[a];
|
|
510
|
+
if (explicitlySet.has(key)) {
|
|
511
|
+
// Apply valueDelimiter splitting for explicitly set args
|
|
512
|
+
if (def.valueDelimiter) {
|
|
513
|
+
const value = result[key];
|
|
514
|
+
if (value !== undefined && (typeof value === 'string' || Array.isArray(value))) {
|
|
515
|
+
result[key] = splitByDelimiter(value, def.valueDelimiter);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
// Env var fallback
|
|
521
|
+
if (def.env) {
|
|
522
|
+
const envValue = globalThis.Bun === undefined
|
|
523
|
+
? process.env[def.env]
|
|
524
|
+
: globalThis.Bun.env[def.env];
|
|
525
|
+
if (envValue !== undefined && envValue !== '') {
|
|
526
|
+
result[key] = def.valueDelimiter
|
|
527
|
+
? envValue.split(def.valueDelimiter)
|
|
528
|
+
: coerceValue(envValue, def, `env:${def.env}`);
|
|
529
|
+
explicitlySet.add(key);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Conditional default (defaultValueIf)
|
|
534
|
+
if (def.defaultValueIf) {
|
|
535
|
+
const [otherKey, otherValue, conditionalDefault] = def.defaultValueIf;
|
|
536
|
+
if (String(result[otherKey]) === String(otherValue)) {
|
|
537
|
+
result[key] = conditionalDefault;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Static default
|
|
542
|
+
if (def.default !== undefined) {
|
|
543
|
+
if (Array.isArray(def.default)) {
|
|
544
|
+
result[key] = [...def.default];
|
|
545
|
+
}
|
|
546
|
+
else if (typeof def.default === 'string' ||
|
|
547
|
+
typeof def.default === 'number' ||
|
|
548
|
+
typeof def.default === 'boolean') {
|
|
549
|
+
result[key] = def.default;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Convert keys to camelCase for the result
|
|
554
|
+
const camelResult = {};
|
|
555
|
+
for (const [key, value] of Object.entries(result)) {
|
|
556
|
+
const camelKey = kebabToCamel(key);
|
|
557
|
+
camelResult[camelKey] = value;
|
|
558
|
+
if (key !== camelKey) {
|
|
559
|
+
camelResult[key] = value;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
args: camelResult,
|
|
564
|
+
positionals,
|
|
565
|
+
rest,
|
|
566
|
+
subCommand,
|
|
567
|
+
helpRequested,
|
|
568
|
+
helpIsShort,
|
|
569
|
+
versionRequested,
|
|
570
|
+
unknown,
|
|
571
|
+
explicitlySet,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// ---- Value Application Helper ----
|
|
575
|
+
/**
|
|
576
|
+
* Apply a raw value from parseArgs into the result, handling count, append,
|
|
577
|
+
* number coercion, boolean conversion, and function valueParser.
|
|
578
|
+
*/
|
|
579
|
+
function applyValue(key, def, rawValue, result, explicitlySet) {
|
|
580
|
+
if (def.action === 'count') {
|
|
581
|
+
if (Array.isArray(rawValue)) {
|
|
582
|
+
result[key] = rawValue.length;
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
result[key] = rawValue === true ? 1 : 0;
|
|
586
|
+
}
|
|
587
|
+
explicitlySet.add(key);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (def.type === 'boolean') {
|
|
591
|
+
result[key] = rawValue === true;
|
|
592
|
+
explicitlySet.add(key);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (def.action === 'append') {
|
|
596
|
+
// node:util parseArgs returns string[] for multiple:true string options
|
|
597
|
+
if (Array.isArray(rawValue)) {
|
|
598
|
+
result[key] = rawValue;
|
|
599
|
+
}
|
|
600
|
+
else if (typeof rawValue === 'string') {
|
|
601
|
+
result[key] = [rawValue];
|
|
602
|
+
}
|
|
603
|
+
explicitlySet.add(key);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
// Single string/number/enum value
|
|
607
|
+
if (typeof rawValue === 'string') {
|
|
608
|
+
result[key] = coerceValue(rawValue, def, `--${def.long ?? key}`);
|
|
609
|
+
explicitlySet.add(key);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
// Boolean value for a string-typed arg (edge case from parseArgs)
|
|
613
|
+
if (typeof rawValue === 'boolean' && rawValue) {
|
|
614
|
+
result[key] = def.defaultMissingValue ?? true;
|
|
615
|
+
explicitlySet.add(key);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ---- Global Args ----
|
|
619
|
+
/**
|
|
620
|
+
* Collect global args from a command.
|
|
621
|
+
* Returns a merged ArgsDef of all global args.
|
|
622
|
+
*/
|
|
623
|
+
export function collectGlobalArgs(command) {
|
|
624
|
+
const globals = {};
|
|
625
|
+
const args = command.args ?? {};
|
|
626
|
+
for (const [key, def] of Object.entries(args)) {
|
|
627
|
+
if (def.global) {
|
|
628
|
+
globals[key] = def;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return globals;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Merge global args into a subcommand's args.
|
|
635
|
+
* Global args from the parent are added to the child unless the child
|
|
636
|
+
* already defines an arg with the same name.
|
|
637
|
+
*/
|
|
638
|
+
export function mergeGlobalArgs(parentGlobals, childArgs) {
|
|
639
|
+
const merged = { ...childArgs };
|
|
640
|
+
for (const [key, def] of Object.entries(parentGlobals)) {
|
|
641
|
+
if (!(key in merged)) {
|
|
642
|
+
merged[key] = def;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return merged;
|
|
646
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command runner - entry point for CLI execution.
|
|
3
|
+
* Handles subcommand resolution, lifecycle hooks, error handling.
|
|
4
|
+
* Supports inferSubcommands, subcommandRequired, allowExternalSubcommands,
|
|
5
|
+
* argsConflictsWithSubcommands, argRequiredElseHelp, and custom styles.
|
|
6
|
+
*/
|
|
7
|
+
import type { ArgDef, ArgsDef, CommandDef, ParsedArgs, RunOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Define a command with full type inference on arguments.
|
|
10
|
+
* This is the primary API for creating commands.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const cmd = defineCommand({
|
|
14
|
+
* meta: { name: 'my-tool', version: '1.0.0', description: 'My tool' },
|
|
15
|
+
* args: {
|
|
16
|
+
* verbose: { type: 'boolean', short: 'v', description: 'Verbose output' },
|
|
17
|
+
* port: { type: 'number', short: 'p', default: 3000, description: 'Port' },
|
|
18
|
+
* },
|
|
19
|
+
* run({ args }) {
|
|
20
|
+
* console.log(args.verbose, args.port);
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function defineCommand<const T extends ArgsDef>(def: CommandDef<T>): CommandDef<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Define a reusable argument group with full type inference.
|
|
28
|
+
* Use this for shared args that are spread into multiple commands.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const envArgs = defineArgs({
|
|
32
|
+
* env: { type: 'string', valueParser: ['dev', 'staging', 'prod'] },
|
|
33
|
+
* dev: { type: 'boolean', conflictsWith: ['env', 'staging', 'prod'] },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function defineArgs<const T extends ArgsDef>(args: T): T;
|
|
38
|
+
/**
|
|
39
|
+
* Define a single argument with full type inference.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const portArg = defineArg({ type: 'number', short: 'p', default: 3003 });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function defineArg<const T extends ArgDef>(arg: T): T;
|
|
46
|
+
/**
|
|
47
|
+
* Run a specific command with pre-parsed arguments.
|
|
48
|
+
* Executes the setup -> run -> cleanup lifecycle.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runCommand<T extends ArgsDef>(command: CommandDef<T>, args: ParsedArgs<T>, rawArgs?: readonly string[], subCommand?: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Main entry point for CLI applications.
|
|
53
|
+
* Parses args, resolves subcommands, validates, and runs.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const main = defineCommand({ ... });
|
|
57
|
+
* runMain(main);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function runMain(rootCommand: CommandDef<any>, opts?: RunOptions): Promise<void>;
|