clap-ts 0.2.0 → 0.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/dist/parser.js CHANGED
@@ -1,265 +1,297 @@
1
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.
2
+ * Argument parser.
6
3
  *
7
- * node:util parseArgs handles:
8
- * --flag, --flag=value, --flag value, -f, -fvalue, -abc (combined booleans),
9
- * -- separator, positionals
4
+ * Tokenizes argv directly against the command's own arg definitions. This
5
+ * replaced node:util parseArgs, which re-validates its entire `options` object
6
+ * on every call (~170ns per option, regardless of argv length) and cannot
7
+ * express multi-value options, value terminators, or subcommand boundaries.
10
8
  *
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.
9
+ * Handled here: long/short/clustered flags, attached and `=` values, boolean
10
+ * negation, count and append actions, multi-token numArgs, optional values via
11
+ * defaultMissingValue, value delimiters, hyphen and negative-number values,
12
+ * positional assignment, trailing var args, `--` escape, subcommand
13
+ * boundaries, env fallback, conditional and static defaults, and
14
+ * kebab-to-camel key mapping.
15
+ *
16
+ * Constraint checks that need the whole picture (conflicts, requires, groups,
17
+ * possible values) live in validation.ts.
19
18
  */
20
- import { parseArgs as nodeParseArgs } from 'node:util';
19
+ // ---- Error ----
20
+ export class CliParseError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'CliParseError';
24
+ }
25
+ }
21
26
  // ---- Helpers ----
27
+ const HYPHEN = 45;
28
+ const NEGATIVE_NUMBER = /^-\d/;
22
29
  /** Convert kebab-case to camelCase: --config-path -> configPath */
23
- function kebabToCamel(s) {
30
+ export function kebabToCamel(s) {
24
31
  return s.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
25
32
  }
26
- /** Get the raw argv slice (after the binary/script path). */
27
- export function getRawArgs(argv) {
33
+ /**
34
+ * Get the raw argv slice (after the binary/script path). With `noBinaryName`
35
+ * the source is taken as-is, matching clap's Command::no_binary_name.
36
+ */
37
+ export function getRawArgs(argv, noBinaryName = false) {
28
38
  if (argv) {
29
39
  return [...argv];
30
40
  }
31
41
  // Bun.argv includes [bun, script, ...args], same as process.argv
32
42
  const source = globalThis.Bun === undefined ? process.argv : globalThis.Bun.argv;
33
- return source.slice(2);
43
+ return noBinaryName ? [...source] : source.slice(2);
34
44
  }
35
- // ---- Error ----
36
- export class CliParseError extends Error {
37
- constructor(message) {
38
- super(message);
39
- this.name = 'CliParseError';
40
- }
45
+ function readEnv(name) {
46
+ return globalThis.Bun === undefined
47
+ ? process.env[name]
48
+ : globalThis.Bun.env[name];
41
49
  }
42
- // ---- Flag Lookup Maps ----
50
+ // ---- Subcommands ----
51
+ const resolvedSubCommands = new WeakMap();
43
52
  /**
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.
53
+ * The command's subcommands, building any lazy ones on first use and caching
54
+ * the result so the thunk runs at most once per command.
47
55
  */
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
- }
56
+ export function subCommandsOf(command) {
57
+ if (command.lazySubCommands === undefined) {
58
+ return command.subCommands ?? {};
59
+ }
60
+ let resolved = resolvedSubCommands.get(command);
61
+ if (resolved === undefined) {
62
+ resolved = { ...command.lazySubCommands(), ...command.subCommands };
63
+ resolvedSubCommands.set(command, resolved);
64
+ }
65
+ return resolved;
66
+ }
67
+ /** Whether the command has any subcommand, without building the lazy ones. */
68
+ export function hasSubCommands(command) {
69
+ if (command.lazySubCommands !== undefined) {
70
+ return true;
71
+ }
72
+ const subs = command.subCommands;
73
+ if (subs === undefined) {
74
+ return false;
86
75
  }
87
- return { longMap, shortMap, hasHyphenOrNegative };
76
+ for (const _key in subs) {
77
+ return true;
78
+ }
79
+ return false;
88
80
  }
89
- // ---- Build parseArgs options config ----
81
+ // ---- Possible Values ----
82
+ const possibleValueCache = new WeakMap();
90
83
  /**
91
- * Build the `options` config that node:util parseArgs expects from our ArgDef definitions.
84
+ * Normalize an arg's allowed values to PossibleValue records. Plain strings and
85
+ * PossibleValue objects can be mixed in the same list.
92
86
  */
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);
87
+ export function possibleValues(def) {
88
+ const parser = def.valueParser;
89
+ if (parser === undefined || typeof parser === 'function') {
90
+ return [];
91
+ }
92
+ const cached = possibleValueCache.get(parser);
93
+ if (cached !== undefined) {
94
+ return cached;
95
+ }
96
+ const normalized = parser.map((v) => (typeof v === 'string' ? { name: v } : v));
97
+ possibleValueCache.set(parser, normalized);
98
+ return normalized;
99
+ }
100
+ /** Whether a raw value matches this possible value, by name or alias. */
101
+ export function matchesPossibleValue(candidate, value, ignoreCase) {
102
+ if (ignoreCase) {
103
+ const lowered = value.toLowerCase();
104
+ if (candidate.name.toLowerCase() === lowered) {
105
+ return true;
137
106
  }
107
+ return candidate.aliases?.some((a) => a.toLowerCase() === lowered) ?? false;
138
108
  }
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 !== '--'));
109
+ return candidate.name === value || (candidate.aliases?.includes(value) ?? false);
149
110
  }
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);
111
+ const specCache = new WeakMap();
112
+ /** Actions that stand alone: the flag carries no value of its own. */
113
+ const VALUELESS_ACTIONS = new Set([
114
+ 'count',
115
+ 'setTrue',
116
+ 'setFalse',
117
+ 'help',
118
+ 'helpShort',
119
+ 'helpLong',
120
+ 'version',
121
+ ]);
122
+ function valueCounts(def) {
123
+ if (def.numArgs) {
124
+ return { min: def.numArgs.min, max: def.numArgs.max };
125
+ }
126
+ if (def.type === 'boolean' || (def.action !== undefined && VALUELESS_ACTIONS.has(def.action))) {
127
+ return { min: 0, max: 0 };
128
+ }
129
+ return { min: 1, max: 1 };
153
130
  }
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)) {
131
+ /** The stderr notice for a deprecated arg, or undefined when it is current. */
132
+ function deprecationNotice(key, def) {
133
+ if (def.deprecated === undefined || def.deprecated === false) {
161
134
  return undefined;
162
135
  }
163
- const entry = longMap.get(flagName);
164
- if (!entry) {
165
- return undefined;
166
- }
167
- return { key: entry.key, defaultVal: entry.def.defaultMissingValue ?? true };
136
+ const label = def.type === 'positional' ? `<${def.valueName ?? key}>` : `--${def.long ?? key}`;
137
+ const reason = typeof def.deprecated === 'string' ? `: ${def.deprecated}` : '';
138
+ const instead = def.replacedBy === undefined ? '' : `; use '--${def.replacedBy}' instead`;
139
+ return `'${label}' is deprecated${reason}${instead}`;
168
140
  }
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 };
141
+ function makeSpec(key, def, argsOverrideSelf) {
142
+ const { min, max } = valueCounts(def);
143
+ const isAppend = def.action === 'append';
144
+ const forced = def.action === 'setTrue' ? true : def.action === 'setFalse' ? false : undefined;
145
+ const triggers = def.action === 'help' || def.action === 'helpShort' || def.action === 'helpLong'
146
+ ? def.action
147
+ : def.action === 'version'
148
+ ? 'version'
149
+ : undefined;
150
+ return {
151
+ forced,
152
+ triggers,
153
+ deprecation: deprecationNotice(key, def),
154
+ key,
155
+ def,
156
+ long: def.long ?? key,
157
+ camel: kebabToCamel(key),
158
+ repeatIsError: !isAppend && !argsOverrideSelf && def.overridesWith === undefined,
159
+ isBool: def.type === 'boolean' && !VALUELESS_ACTIONS.has(def.action ?? ''),
160
+ isCount: def.action === 'count',
161
+ isAppend,
162
+ min,
163
+ max,
164
+ };
184
165
  }
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;
166
+ function registerAliases(aliases, spec, longs, shorts) {
167
+ if (!aliases) {
168
+ return;
194
169
  }
195
- if (token.startsWith('-') && token.length === 2 && token[1] !== '-') {
196
- return shortMap.get(token[1]) ?? undefined;
170
+ for (const alias of aliases) {
171
+ if (alias.length === 1) {
172
+ shorts.set(alias, spec);
173
+ }
174
+ else {
175
+ longs.set(alias, { spec, negated: false });
176
+ }
197
177
  }
198
- return undefined;
199
178
  }
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);
179
+ function buildSpec(command) {
180
+ const argsDef = command.args ?? {};
181
+ const longs = new Map();
182
+ const shorts = new Map();
183
+ const positionals = [];
184
+ const all = [];
185
+ const byKey = {};
186
+ let allowsNegative = false;
187
+ const cmdAllowHyphen = command.meta.allowHyphenValues === true;
188
+ const argsOverrideSelf = command.meta.argsOverrideSelf === true;
189
+ let hasOverrides = false;
190
+ if (command.meta.allowNegativeNumbers) {
191
+ allowsNegative = true;
192
+ }
193
+ for (const key of Object.keys(argsDef)) {
194
+ const def = argsDef[key];
195
+ if (def.allowNegativeNumbers) {
196
+ allowsNegative = true;
197
+ }
198
+ if (def.overridesWith !== undefined) {
199
+ hasOverrides = true;
200
+ }
201
+ const spec = makeSpec(key, def, argsOverrideSelf);
202
+ all.push(spec);
203
+ byKey[key] = spec;
204
+ if (def.type === 'positional') {
205
+ positionals.push(spec);
220
206
  continue;
221
207
  }
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;
208
+ longs.set(spec.long, { spec, negated: false });
209
+ if (spec.isBool) {
210
+ longs.set(`no-${spec.long}`, { spec, negated: true });
228
211
  }
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;
212
+ if (def.short && def.short.length === 1) {
213
+ shorts.set(def.short, spec);
234
214
  }
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
- }
215
+ registerAliases(def.alias, spec, longs, shorts);
216
+ registerAliases(def.visibleAlias, spec, longs, shorts);
217
+ }
218
+ // Built-in flags never displace a user-defined arg of the same name.
219
+ const helpSpec = {
220
+ key: 'help', def: { type: 'boolean' }, long: 'help', camel: 'help',
221
+ repeatIsError: false,
222
+ isBool: true, isCount: false, isAppend: false, min: 0, max: 0, builtin: 'help',
223
+ };
224
+ const versionSpec = {
225
+ key: 'version', def: { type: 'boolean' }, long: 'version', camel: 'version',
226
+ repeatIsError: false,
227
+ isBool: true, isCount: false, isAppend: false, min: 0, max: 0, builtin: 'version',
228
+ };
229
+ if (command.meta.disableHelpFlag !== true) {
230
+ if (!longs.has('help')) {
231
+ longs.set('help', { spec: helpSpec, negated: false });
232
+ }
233
+ if (!shorts.has('h')) {
234
+ shorts.set('h', helpSpec);
251
235
  }
252
- processedArgs.push(token);
253
236
  }
254
- return { processedArgs, optionalDefaults };
237
+ // clap only offers --version where a version exists, either on this command
238
+ // or propagated down from an ancestor.
239
+ if (command.meta.disableVersionFlag !== true && command.meta.version !== undefined) {
240
+ if (!longs.has('version')) {
241
+ longs.set('version', { spec: versionSpec, negated: false });
242
+ }
243
+ if (!shorts.has('V')) {
244
+ shorts.set('V', versionSpec);
245
+ }
246
+ }
247
+ if (command.meta.helpExpected) {
248
+ const undocumented = all
249
+ .filter((spec) => !spec.def.hidden && !spec.def.description && !spec.def.longDescription)
250
+ .map((spec) => spec.key);
251
+ if (undocumented.length > 0) {
252
+ throw new CliParseError(`helpExpected is set but these arguments have no description: ${undocumented.join(', ')}`);
253
+ }
254
+ }
255
+ // An explicit index overrides declaration order; unindexed positionals keep
256
+ // their relative order after the indexed ones are placed.
257
+ if (positionals.some((spec) => spec.def.index !== undefined)) {
258
+ positionals.sort((a, b) => (a.def.index ?? Number.MAX_SAFE_INTEGER) - (b.def.index ?? Number.MAX_SAFE_INTEGER));
259
+ }
260
+ return {
261
+ longs,
262
+ shorts,
263
+ positionals,
264
+ indexedPositionals: positionals.reduce((n, spec) => (spec.def.last ? n : n + 1), 0),
265
+ all,
266
+ byKey,
267
+ command,
268
+ anySubcommands: hasSubCommands(command),
269
+ inferSubcommands: command.meta.inferSubcommands === true,
270
+ allowExternal: command.meta.allowExternalSubcommands === true,
271
+ subcommandPrecedence: command.meta.subcommandPrecedenceOverArg === true,
272
+ allowMissingPositional: command.meta.allowMissingPositional === true,
273
+ argsOverrideSelf,
274
+ ignoreErrors: command.meta.ignoreErrors === true,
275
+ dontDelimitTrailingValues: command.meta.dontDelimitTrailingValues === true,
276
+ hasOverrides,
277
+ cmdAllowHyphen,
278
+ allowsNegative,
279
+ inferLong: command.meta.inferLongArgs === true,
280
+ };
255
281
  }
256
- // ---- Coerce Value ----
257
- function coerceValue(value, def, argName) {
258
- // If a function valueParser is defined, use it for coercion
282
+ function getSpec(command) {
283
+ let spec = specCache.get(command);
284
+ if (spec === undefined) {
285
+ spec = buildSpec(command);
286
+ specCache.set(command, spec);
287
+ }
288
+ return spec;
289
+ }
290
+ // ---- Value Coercion ----
291
+ export function coerceValue(value, def, argName) {
259
292
  if (typeof def.valueParser === 'function') {
260
293
  try {
261
294
  const parsed = def.valueParser(value);
262
- // Ensure the result is a valid ParseResult value type
263
295
  if (typeof parsed === 'string' ||
264
296
  typeof parsed === 'number' ||
265
297
  typeof parsed === 'boolean') {
@@ -291,225 +323,488 @@ function coerceValue(value, def, argName) {
291
323
  }
292
324
  }
293
325
  }
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
- }
326
+ /** Human-readable name for this arg in error messages. */
327
+ function displayName(spec) {
328
+ return spec.def.type === 'positional' ? `<${spec.def.valueName ?? spec.key}>` : `--${spec.long}`;
329
+ }
330
+ /** Record that an arg was explicitly given, keeping the occurrence order. */
331
+ function markSet(state, key, spec) {
332
+ state.explicitlySet.add(key);
333
+ state.valueSources.set(key, 'cli');
334
+ state.order?.set(key, state.seq++);
335
+ if (spec?.deprecation !== undefined && !state.warned.has(key)) {
336
+ state.warned.add(key);
337
+ state.warnings.push(spec.deprecation);
308
338
  }
309
- return map;
310
339
  }
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;
340
+ /** Whether a token can serve as a value for this arg rather than starting a new flag. */
341
+ function isValueToken(token, spec, cmdSpec) {
342
+ if (token.length === 0 || token.charCodeAt(0) !== HYPHEN) {
343
+ return true;
318
344
  }
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
- }
345
+ if (token === '-') {
346
+ return true;
347
+ }
348
+ if (token === '--') {
349
+ return false;
325
350
  }
326
- return undefined;
351
+ if (spec.def.allowHyphenValues || cmdSpec.cmdAllowHyphen) {
352
+ return true;
353
+ }
354
+ return ((spec.def.allowNegativeNumbers || cmdSpec.allowsNegative) && NEGATIVE_NUMBER.test(token));
355
+ }
356
+ /** Record a flag occurrence that carries no value: booleans and counts. */
357
+ function applyFlagOnly(spec, negated, state) {
358
+ if (spec.builtin === 'help' || spec.triggers === 'help' || spec.triggers === 'helpLong') {
359
+ state.helpRequested = true;
360
+ return;
361
+ }
362
+ if (spec.triggers === 'helpShort') {
363
+ state.helpRequested = true;
364
+ state.helpIsShort = true;
365
+ return;
366
+ }
367
+ if (spec.builtin === 'version' || spec.triggers === 'version') {
368
+ state.versionRequested = true;
369
+ return;
370
+ }
371
+ if (spec.forced !== undefined) {
372
+ state.result[spec.key] = negated ? !spec.forced : spec.forced;
373
+ markSet(state, spec.key, spec);
374
+ return;
375
+ }
376
+ if (spec.isCount) {
377
+ const previous = state.result[spec.key];
378
+ state.result[spec.key] = (typeof previous === 'number' ? previous : 0) + 1;
379
+ }
380
+ else {
381
+ state.result[spec.key] = !negated;
382
+ }
383
+ markSet(state, spec.key, spec);
384
+ }
385
+ /** Store one or more parsed values for an arg, honouring the append action. */
386
+ function applyValues(spec, values, state) {
387
+ const name = displayName(spec);
388
+ // A second occurrence of a single-value arg is an error in clap unless the
389
+ // command opted into argsOverrideSelf. Reading result is cheaper than the
390
+ // Set, and defaults have not been applied yet, so a value here means a
391
+ // previous occurrence.
392
+ if (spec.repeatIsError && state.result[spec.key] !== undefined) {
393
+ throw new CliParseError(`the argument '${name}' cannot be used multiple times`);
394
+ }
395
+ if (spec.isAppend) {
396
+ const previous = state.result[spec.key];
397
+ const list = Array.isArray(previous) ? previous : [];
398
+ for (const value of values) {
399
+ list.push(String(coerceValue(value, spec.def, name)));
400
+ }
401
+ state.result[spec.key] = list;
402
+ }
403
+ else if (values.length > 1) {
404
+ state.result[spec.key] = values.map((v) => String(coerceValue(v, spec.def, name)));
405
+ }
406
+ else {
407
+ state.result[spec.key] = coerceValue(values[0], spec.def, name);
408
+ }
409
+ markSet(state, spec.key, spec);
410
+ }
411
+ /** Apply defaultMissingValue for a flag whose value was omitted (numArgs.min === 0). */
412
+ function applyMissingValue(spec, state) {
413
+ if (spec.def.defaultMissingValues !== undefined) {
414
+ state.result[spec.key] = [...spec.def.defaultMissingValues];
415
+ markSet(state, spec.key, spec);
416
+ return;
417
+ }
418
+ const fallback = spec.def.defaultMissingValue ?? true;
419
+ if (spec.isAppend) {
420
+ const previous = state.result[spec.key];
421
+ const list = Array.isArray(previous) ? previous : [];
422
+ list.push(String(fallback));
423
+ state.result[spec.key] = list;
424
+ }
425
+ else {
426
+ state.result[spec.key] = fallback;
427
+ }
428
+ markSet(state, spec.key, spec);
327
429
  }
328
- // ---- Separate Rest Args ----
329
430
  /**
330
- * parseArgs lumps everything (before and after --) into its `positionals` array.
331
- * We need to split them into positionals (before --) and rest (after --).
431
+ * Consume up to `spec.max` values for a flag starting at argv[from].
432
+ * Returns the index of the last token consumed.
332
433
  */
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;
434
+ function consumeValues(spec, argv, from, cmdSpec, state) {
435
+ const values = [];
436
+ let i = from;
437
+ while (values.length < spec.max && i < argv.length) {
438
+ const token = argv[i];
439
+ if (spec.def.valueTerminator !== undefined && token === spec.def.valueTerminator) {
440
+ i++;
441
+ break;
442
+ }
443
+ if (!isValueToken(token, spec, cmdSpec)) {
444
+ break;
445
+ }
446
+ if (cmdSpec.subcommandPrecedence &&
447
+ values.length >= spec.min &&
448
+ resolveSubcommand(cmdSpec, token) !== undefined) {
338
449
  break;
339
450
  }
451
+ values.push(token);
452
+ i++;
340
453
  }
341
- if (dashDashIdx === -1) {
342
- return { positionals: [...parseArgsPositionals], rest: [] };
454
+ if (values.length < spec.min) {
455
+ if (values.length === 0 && spec.min === 0) {
456
+ applyMissingValue(spec, state);
457
+ return i - 1;
458
+ }
459
+ throw new CliParseError(spec.min === 1
460
+ ? `a value is required for '${displayName(spec)}' but none was supplied`
461
+ : `the argument '${displayName(spec)}' requires at least ${String(spec.min)} values but ${String(values.length)} were provided`);
343
462
  }
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
- };
463
+ if (values.length === 0) {
464
+ applyMissingValue(spec, state);
465
+ }
466
+ else {
467
+ applyValues(spec, values, state);
468
+ }
469
+ return i - 1;
351
470
  }
352
- // ---- Infer Long Args ----
353
471
  /**
354
- * Try to resolve an unknown long flag by prefix matching.
355
- * Returns the matched entry if exactly one match, undefined otherwise.
472
+ * Resolve an unknown long flag by unique prefix match (inferLongArgs).
473
+ * Negated (`--no-x`) entries are excluded so `--n` cannot silently negate.
356
474
  */
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);
475
+ function inferLongEntry(cmdSpec, name) {
476
+ let match;
477
+ for (const [candidate, entry] of cmdSpec.longs) {
478
+ if (entry.negated || !candidate.startsWith(name)) {
479
+ continue;
480
+ }
481
+ if (match !== undefined) {
482
+ return undefined;
362
483
  }
484
+ match = entry;
363
485
  }
364
- return matches.length === 1 ? matches[0] : undefined;
486
+ return match;
365
487
  }
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;
488
+ /** Build the name and flag lookup maps, once, on the first token that needs them. */
489
+ function buildSubcommandMaps(cmdSpec) {
490
+ const names = new Map();
491
+ const flags = new Map();
492
+ const subs = subCommandsOf(cmdSpec.command);
493
+ for (const name of Object.keys(subs)) {
494
+ names.set(name, name);
495
+ const subMeta = subs[name].meta;
496
+ for (const alias of [...(subMeta.aliases ?? []), ...(subMeta.hiddenAliases ?? [])]) {
497
+ names.set(alias, name);
498
+ }
499
+ for (const form of [
500
+ ...(subMeta.shortFlag === undefined ? [] : [subMeta.shortFlag]),
501
+ ...(subMeta.shortFlagAliases ?? []),
502
+ ...(subMeta.visibleShortFlagAliases ?? []),
503
+ ]) {
504
+ flags.set(`-${form}`, name);
505
+ }
506
+ for (const form of [
507
+ ...(subMeta.longFlag === undefined ? [] : [subMeta.longFlag]),
508
+ ...(subMeta.longFlagAliases ?? []),
509
+ ...(subMeta.visibleLongFlagAliases ?? []),
510
+ ]) {
511
+ flags.set(`--${form}`, name);
512
+ }
513
+ }
514
+ cmdSpec.subcommands = names;
515
+ cmdSpec.subcommandFlags = flags;
516
+ }
517
+ /** The subcommand named by a flag form like `-S`, or undefined. */
518
+ function subcommandForFlag(cmdSpec, token) {
519
+ if (!cmdSpec.anySubcommands) {
520
+ return undefined;
378
521
  }
379
- return value.split(delimiter);
522
+ if (cmdSpec.subcommandFlags === undefined) {
523
+ buildSubcommandMaps(cmdSpec);
524
+ }
525
+ return cmdSpec.subcommandFlags.get(token);
380
526
  }
381
- // ---- Main Parser ----
382
527
  /**
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.
528
+ * Resolve a bare token to a canonical subcommand name, by exact match on the
529
+ * name or an alias, then by unique prefix when inferSubcommands is on.
386
530
  */
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') {
531
+ function resolveSubcommand(cmdSpec, token) {
532
+ if (!cmdSpec.anySubcommands) {
533
+ return undefined;
534
+ }
535
+ if (cmdSpec.subcommands === undefined) {
536
+ buildSubcommandMaps(cmdSpec);
537
+ }
538
+ const map = cmdSpec.subcommands;
539
+ const exact = map.get(token);
540
+ if (exact !== undefined) {
541
+ return exact;
542
+ }
543
+ if (!cmdSpec.inferSubcommands) {
544
+ return undefined;
545
+ }
546
+ let match;
547
+ for (const [candidate, canonical] of map) {
548
+ if (!candidate.startsWith(token)) {
421
549
  continue;
422
550
  }
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;
551
+ if (match !== undefined && match !== canonical) {
552
+ return undefined;
553
+ }
554
+ match = canonical;
555
+ }
556
+ return match;
557
+ }
558
+ /** Handle a `--long`, `--long=value` or `--no-long` token. Returns the last index consumed. */
559
+ function handleLong(token, argv, i, cmdSpec, state) {
560
+ const eq = token.indexOf('=');
561
+ const name = eq === -1 ? token.slice(2) : token.slice(2, eq);
562
+ let entry = cmdSpec.longs.get(name);
563
+ if (entry === undefined && cmdSpec.inferLong) {
564
+ entry = inferLongEntry(cmdSpec, name);
565
+ }
566
+ if (entry === undefined) {
567
+ // Only now is it worth building the subcommand maps: an unrecognised flag
568
+ // may still be a flag-invoked subcommand like `pacman --sync`.
569
+ const flagSub = subcommandForFlag(cmdSpec, token);
570
+ if (flagSub !== undefined) {
571
+ state.subCommand = flagSub;
572
+ state.subCommandArgs = argv.slice(i + 1);
573
+ return i;
574
+ }
575
+ state.unknown.push(`--${name}`);
576
+ return i;
577
+ }
578
+ const { spec, negated } = entry;
579
+ if (eq !== -1) {
580
+ const inline = token.slice(eq + 1);
581
+ if (spec.builtin !== undefined || spec.isCount) {
582
+ applyFlagOnly(spec, negated, state);
583
+ return i;
584
+ }
585
+ if (spec.isBool && negated) {
586
+ // `--no-verbose=true` means "negate", so invert whatever was supplied.
587
+ state.result[spec.key] = coerceValue(inline, spec.def, `--${name}`) === false;
588
+ markSet(state, spec.key, spec);
589
+ return i;
590
+ }
591
+ applyValues(spec, [inline], state);
592
+ return i;
593
+ }
594
+ if (spec.max === 0) {
595
+ applyFlagOnly(spec, negated, state);
596
+ return i;
597
+ }
598
+ if (spec.def.requireEquals) {
599
+ if (spec.min === 0) {
600
+ applyMissingValue(spec, state);
601
+ return i;
602
+ }
603
+ throw new CliParseError(`equal sign is needed when assigning values to '${displayName(spec)}'`);
604
+ }
605
+ return consumeValues(spec, argv, i + 1, cmdSpec, state);
606
+ }
607
+ /** Handle a `-abc`, `-p80`, `-p=80` or `-p 80` token. Returns the last index consumed. */
608
+ function handleShort(token, argv, i, cmdSpec, state) {
609
+ for (let c = 1; c < token.length; c++) {
610
+ const flag = token[c];
611
+ const spec = cmdSpec.shorts.get(flag);
612
+ if (spec === undefined) {
613
+ const flagSub = subcommandForFlag(cmdSpec, token);
614
+ if (flagSub !== undefined) {
615
+ state.subCommand = flagSub;
616
+ state.subCommandArgs = argv.slice(i + 1);
617
+ return i;
431
618
  }
619
+ state.unknown.push(`-${flag}`);
620
+ return i;
432
621
  }
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);
622
+ if (spec.builtin === 'help') {
623
+ state.helpRequested = true;
624
+ state.helpIsShort = true;
437
625
  continue;
438
626
  }
439
- // Check short map
440
- const shortEntry = shortMap.get(parsedKey);
441
- if (shortEntry) {
442
- applyValue(shortEntry.key, shortEntry.def, rawValue, result, explicitlySet);
627
+ if (spec.builtin === 'version') {
628
+ state.versionRequested = true;
629
+ state.versionIsShort = true;
443
630
  continue;
444
631
  }
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;
632
+ if (spec.max === 0) {
633
+ applyFlagOnly(spec, false, state);
634
+ continue;
635
+ }
636
+ // A value-taking short flag takes the rest of the cluster, or the next token.
637
+ if (c + 1 < token.length) {
638
+ // clap strips a single leading '=' so `-o=v` and `-ov` agree.
639
+ const attached = token.charCodeAt(c + 1) === 61 ? token.slice(c + 2) : token.slice(c + 1);
640
+ applyValues(spec, [attached], state);
641
+ return i;
642
+ }
643
+ if (spec.def.requireEquals) {
644
+ if (spec.min === 0) {
645
+ applyMissingValue(spec, state);
646
+ return i;
451
647
  }
648
+ throw new CliParseError(`equal sign is needed when assigning values to '${displayName(spec)}'`);
452
649
  }
453
- unknown.push(`--${parsedKey}`);
650
+ return consumeValues(spec, argv, i + 1, cmdSpec, state);
454
651
  }
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);
652
+ return i;
653
+ }
654
+ /**
655
+ * Consume one token: a flag-invoked subcommand, a long or short flag, a
656
+ * subcommand boundary, or a positional. Returns the last index consumed.
657
+ */
658
+ function handleToken(token, rawArgs, i, cmdSpec, state) {
659
+ if (token.length > 1 && token.charCodeAt(0) === HYPHEN) {
660
+ if (token.charCodeAt(1) === HYPHEN) {
661
+ return handleLong(token, rawArgs, i, cmdSpec, state);
662
+ }
663
+ // A negative number is a value, not a flag cluster, when nothing claims the
664
+ // leading digit as a short flag.
665
+ const isNegativeValue = cmdSpec.allowsNegative && NEGATIVE_NUMBER.test(token) && !cmdSpec.shorts.has(token[1]);
666
+ if (!isNegativeValue) {
667
+ return handleShort(token, rawArgs, i, cmdSpec, state);
668
+ }
669
+ }
670
+ if (state.positionals.length === 0) {
671
+ const canonical = resolveSubcommand(cmdSpec, token);
672
+ if (canonical !== undefined) {
673
+ state.subCommand = canonical;
674
+ state.subCommandArgs = rawArgs.slice(i + 1);
675
+ return i;
676
+ }
677
+ if (cmdSpec.allowExternal) {
678
+ state.subCommand = token;
679
+ state.subCommandIsExternal = true;
680
+ state.subCommandArgs = rawArgs.slice(i + 1);
681
+ return i;
682
+ }
683
+ }
684
+ state.positionals.push(token);
685
+ return i;
686
+ }
687
+ // ---- Positional Assignment ----
688
+ function assignPositionals(cmdSpec, state) {
689
+ let index = 0;
690
+ // Definitions still waiting for a value, so a leading optional one can be
691
+ // skipped when there are not enough values to go round.
692
+ let remainingDefs = cmdSpec.indexedPositionals;
693
+ for (const spec of cmdSpec.positionals) {
694
+ if (!spec.def.last) {
695
+ remainingDefs--;
696
+ if (cmdSpec.allowMissingPositional &&
697
+ !spec.def.required &&
698
+ state.positionals.length - index <= remainingDefs) {
699
+ continue;
700
+ }
460
701
  }
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) {
702
+ if (spec.def.last) {
703
+ // `last` positionals are only fed from tokens after `--`.
704
+ if (state.rest.length > 0) {
705
+ state.result[spec.key] = state.rest[0];
706
+ state.fromRest.add(spec.key);
707
+ markSet(state, spec.key, spec);
708
+ }
471
709
  continue;
472
710
  }
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 });
711
+ if (spec.def.trailingVarArg) {
712
+ if (index < state.positionals.length) {
713
+ state.result[spec.key] = state.positionals.slice(index);
714
+ markSet(state, spec.key, spec);
715
+ index = state.positionals.length;
716
+ }
717
+ return;
718
+ }
719
+ if (index >= state.positionals.length) {
720
+ continue;
480
721
  }
722
+ if (spec.max > 1) {
723
+ const take = Math.min(spec.max, state.positionals.length - index);
724
+ const values = state.positionals.slice(index, index + take);
725
+ state.result[spec.key] = values.map((v) => String(coerceValue(v, spec.def, spec.key)));
726
+ markSet(state, spec.key, spec);
727
+ index += take;
728
+ continue;
729
+ }
730
+ state.result[spec.key] = coerceValue(state.positionals[index], spec.def, spec.key);
731
+ markSet(state, spec.key, spec);
732
+ index++;
481
733
  }
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
- }
734
+ }
735
+ // ---- Overrides ----
736
+ /**
737
+ * Drop args displaced by a later `overridesWith`. Only command-line
738
+ * occurrences take part, so an env or default value is never overridden.
739
+ */
740
+ function applyOverrides(cmdSpec, state) {
741
+ const order = state.order;
742
+ if (order === undefined) {
743
+ return;
744
+ }
745
+ for (const spec of cmdSpec.all) {
746
+ const targets = spec.def.overridesWith;
747
+ if (targets === undefined || !state.explicitlySet.has(spec.key)) {
490
748
  continue;
491
749
  }
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);
750
+ const mine = order.get(spec.key);
751
+ if (mine === undefined) {
752
+ continue;
753
+ }
754
+ for (const target of targets) {
755
+ if (target === spec.key) {
756
+ continue;
757
+ }
758
+ const theirs = order.get(target);
759
+ if (theirs === undefined || theirs > mine) {
760
+ continue;
497
761
  }
498
- break; // no more positional defs to process
762
+ delete state.result[target];
763
+ state.explicitlySet.delete(target);
764
+ order.delete(target);
765
+ }
766
+ }
767
+ }
768
+ /**
769
+ * Copy each deprecated arg's value to the arg that replaced it, unless that one
770
+ * was given directly. Lets a rename keep working without the handler caring.
771
+ */
772
+ function applyReplacements(cmdSpec, state) {
773
+ for (const spec of cmdSpec.all) {
774
+ const target = spec.def.replacedBy;
775
+ if (target === undefined || !state.explicitlySet.has(spec.key)) {
776
+ continue;
777
+ }
778
+ if (state.explicitlySet.has(target) || !(target in cmdSpec.byKey)) {
779
+ continue;
499
780
  }
500
- // Normal positional assignment
501
- if (p < positionals.length) {
502
- result[key] = coerceValue(positionals[p], def, key);
503
- explicitlySet.add(key);
781
+ const value = state.result[spec.key];
782
+ if (value === undefined) {
783
+ continue;
504
784
  }
785
+ state.result[target] = value;
786
+ state.explicitlySet.add(target);
787
+ state.valueSources.set(target, state.valueSources.get(spec.key) ?? 'cli');
505
788
  }
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];
789
+ }
790
+ // ---- Defaults, Env, Delimiters ----
791
+ function splitByDelimiter(value, delimiter) {
792
+ if (Array.isArray(value)) {
793
+ const out = [];
794
+ for (const v of value) {
795
+ out.push(...v.split(delimiter));
796
+ }
797
+ return out;
798
+ }
799
+ return value.split(delimiter);
800
+ }
801
+ function applyFallbacks(cmdSpec, state) {
802
+ const { result, explicitlySet } = state;
803
+ for (const spec of cmdSpec.all) {
804
+ const { key, def } = spec;
510
805
  if (explicitlySet.has(key)) {
511
- // Apply valueDelimiter splitting for explicitly set args
512
- if (def.valueDelimiter) {
806
+ if (def.valueDelimiter &&
807
+ !(cmdSpec.dontDelimitTrailingValues && state.fromRest.has(key))) {
513
808
  const value = result[key];
514
809
  if (value !== undefined && (typeof value === 'string' || Array.isArray(value))) {
515
810
  result[key] = splitByDelimiter(value, def.valueDelimiter);
@@ -517,103 +812,136 @@ export function parseArgs(rawArgs, command) {
517
812
  }
518
813
  continue;
519
814
  }
520
- // Env var fallback
521
815
  if (def.env) {
522
- const envValue = globalThis.Bun === undefined
523
- ? process.env[def.env]
524
- : globalThis.Bun.env[def.env];
816
+ const envValue = readEnv(def.env);
525
817
  if (envValue !== undefined && envValue !== '') {
526
818
  result[key] = def.valueDelimiter
527
819
  ? envValue.split(def.valueDelimiter)
528
820
  : coerceValue(envValue, def, `env:${def.env}`);
529
821
  explicitlySet.add(key);
822
+ state.valueSources.set(key, 'env');
530
823
  continue;
531
824
  }
532
825
  }
533
- // Conditional default (defaultValueIf)
534
826
  if (def.defaultValueIf) {
535
827
  const [otherKey, otherValue, conditionalDefault] = def.defaultValueIf;
536
828
  if (String(result[otherKey]) === String(otherValue)) {
537
829
  result[key] = conditionalDefault;
830
+ state.valueSources.set(key, 'default');
538
831
  continue;
539
832
  }
540
833
  }
541
- // Static default
542
- if (def.default !== undefined) {
543
- if (Array.isArray(def.default)) {
544
- result[key] = [...def.default];
834
+ if (def.defaultValueIfs) {
835
+ let matched = false;
836
+ for (const [otherKey, otherValue, conditionalDefault] of def.defaultValueIfs) {
837
+ if (String(result[otherKey]) === String(otherValue)) {
838
+ result[key] = conditionalDefault;
839
+ state.valueSources.set(key, 'default');
840
+ matched = true;
841
+ break;
842
+ }
545
843
  }
546
- else if (typeof def.default === 'string' ||
547
- typeof def.default === 'number' ||
548
- typeof def.default === 'boolean') {
549
- result[key] = def.default;
844
+ if (matched) {
845
+ continue;
550
846
  }
551
847
  }
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;
848
+ if (def.default !== undefined) {
849
+ result[key] = Array.isArray(def.default)
850
+ ? [...def.default]
851
+ : def.default;
852
+ state.valueSources.set(key, 'default');
560
853
  }
561
854
  }
562
- return {
563
- args: camelResult,
564
- positionals,
565
- rest,
566
- subCommand,
567
- helpRequested,
568
- helpIsShort,
569
- versionRequested,
570
- unknown,
571
- explicitlySet,
572
- };
573
855
  }
574
- // ---- Value Application Helper ----
856
+ // ---- Main Parser ----
575
857
  /**
576
- * Apply a raw value from parseArgs into the result, handling count, append,
577
- * number coercion, boolean conversion, and function valueParser.
858
+ * Parse raw argument tokens against a command definition.
859
+ *
860
+ * Stops at the first bare token matching a subcommand name or alias; the
861
+ * remaining tokens are returned as `subCommandArgs` for the caller to parse
862
+ * against that subcommand.
578
863
  */
579
- function applyValue(key, def, rawValue, result, explicitlySet) {
580
- if (def.action === 'count') {
581
- if (Array.isArray(rawValue)) {
582
- result[key] = rawValue.length;
864
+ export function parseArgs(rawArgs, command) {
865
+ const cmdSpec = getSpec(command);
866
+ const state = {
867
+ result: {},
868
+ explicitlySet: new Set(),
869
+ valueSources: new Map(),
870
+ errors: [],
871
+ warnings: [],
872
+ warned: new Set(),
873
+ fromRest: new Set(),
874
+ order: cmdSpec.hasOverrides ? new Map() : undefined,
875
+ seq: 0,
876
+ unknown: [],
877
+ positionals: [],
878
+ rest: [],
879
+ helpRequested: false,
880
+ helpIsShort: false,
881
+ versionRequested: false,
882
+ versionIsShort: false,
883
+ subCommandIsExternal: false,
884
+ subCommandArgs: [],
885
+ };
886
+ let i = 0;
887
+ for (; i < rawArgs.length; i++) {
888
+ const token = rawArgs[i];
889
+ if (token === '--') {
890
+ for (let r = i + 1; r < rawArgs.length; r++) {
891
+ state.rest.push(rawArgs[r]);
892
+ }
893
+ break;
894
+ }
895
+ // ignoreErrors keeps going after a bad token, collecting the message, the
896
+ // way clap's Command::ignore_errors does.
897
+ if (cmdSpec.ignoreErrors) {
898
+ try {
899
+ i = handleToken(token, rawArgs, i, cmdSpec, state);
900
+ }
901
+ catch (error) {
902
+ state.errors.push(error instanceof Error ? error.message : String(error));
903
+ }
583
904
  }
584
905
  else {
585
- result[key] = rawValue === true ? 1 : 0;
906
+ i = handleToken(token, rawArgs, i, cmdSpec, state);
907
+ }
908
+ if (state.subCommand !== undefined) {
909
+ break;
586
910
  }
587
- explicitlySet.add(key);
588
- return;
589
- }
590
- if (def.type === 'boolean') {
591
- result[key] = rawValue === true;
592
- explicitlySet.add(key);
593
- return;
594
911
  }
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;
912
+ assignPositionals(cmdSpec, state);
913
+ applyOverrides(cmdSpec, state);
914
+ applyReplacements(cmdSpec, state);
915
+ applyFallbacks(cmdSpec, state);
916
+ // Expose both the declared key and its camelCase form.
917
+ const args = {};
918
+ for (const spec of cmdSpec.all) {
919
+ const value = state.result[spec.key];
920
+ if (value === undefined) {
921
+ continue;
599
922
  }
600
- else if (typeof rawValue === 'string') {
601
- result[key] = [rawValue];
923
+ args[spec.camel] = value;
924
+ if (spec.key !== spec.camel) {
925
+ args[spec.key] = value;
602
926
  }
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
927
  }
928
+ return {
929
+ args,
930
+ positionals: state.positionals,
931
+ rest: state.rest,
932
+ subCommand: state.subCommand,
933
+ subCommandIsExternal: state.subCommandIsExternal,
934
+ subCommandArgs: state.subCommandArgs,
935
+ helpRequested: state.helpRequested,
936
+ helpIsShort: state.helpIsShort,
937
+ versionRequested: state.versionRequested,
938
+ versionIsShort: state.versionIsShort,
939
+ unknown: state.unknown,
940
+ errors: state.errors,
941
+ warnings: state.warnings,
942
+ explicitlySet: state.explicitlySet,
943
+ valueSources: state.valueSources,
944
+ };
617
945
  }
618
946
  // ---- Global Args ----
619
947
  /**