@ontrails/commander 1.0.0-beta.39 → 1.0.0-beta.41
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 +13 -0
- package/package.json +3 -3
- package/src/to-commander.ts +226 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @ontrails/commander
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.41
|
|
4
|
+
|
|
5
|
+
## 1.0.0-beta.40
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- [`4030698`](https://github.com/outfitter-dev/trails/commit/40306984467625844564f0f84156530d7118a79c): Keep structured input on nested child commands from being reinterpreted as a
|
|
10
|
+
bare child-name positional fallback, while preserving schema-authored
|
|
11
|
+
`inputJson` flags as ordinary trail input, including through the public Trails
|
|
12
|
+
CLI. Optional numeric flags now consume negative values with Commander's own
|
|
13
|
+
parsing semantics, and variadic flags consume every following value, before
|
|
14
|
+
nested command routing is resolved.
|
|
15
|
+
|
|
3
16
|
## 1.0.0-beta.39
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/commander",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.41",
|
|
4
4
|
"files": [
|
|
5
5
|
"src/**/*.ts",
|
|
6
6
|
"!src/**/__tests__/**",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
26
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
25
|
+
"@ontrails/cli": "^1.0.0-beta.41",
|
|
26
|
+
"@ontrails/core": "^1.0.0-beta.41",
|
|
27
27
|
"commander": "^14.0.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
package/src/to-commander.ts
CHANGED
|
@@ -84,6 +84,8 @@ const buildRepeatableArrayParser =
|
|
|
84
84
|
return [...(Array.isArray(previous) ? previous : []), parsed];
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
const structuredInputOptions = new WeakSet<Option>();
|
|
88
|
+
|
|
87
89
|
/** Apply common modifiers (choices, default, arg parser) to a Commander Option. */
|
|
88
90
|
const applyOptionModifiers = (opt: Option, flag: CliFlag): void => {
|
|
89
91
|
if (isRepeatableArrayFlag(flag)) {
|
|
@@ -107,6 +109,9 @@ const applyOptionModifiers = (opt: Option, flag: CliFlag): void => {
|
|
|
107
109
|
/** Build Commander Option(s) from a CliFlag. Returns one or two options. */
|
|
108
110
|
const buildOptions = (flag: CliFlag): Option[] => {
|
|
109
111
|
const opt = new Option(buildFlagString(flag), flag.description);
|
|
112
|
+
if (flag.role === 'structured-input') {
|
|
113
|
+
structuredInputOptions.add(opt);
|
|
114
|
+
}
|
|
110
115
|
applyOptionModifiers(opt, flag);
|
|
111
116
|
const valueAliasOptions = (flag.valueAliases ?? []).map(
|
|
112
117
|
(alias) =>
|
|
@@ -198,6 +203,199 @@ const INHERITED_SURFACE_OPTION_KEYS = new Set([
|
|
|
198
203
|
'trace',
|
|
199
204
|
'watch',
|
|
200
205
|
]);
|
|
206
|
+
const commandPathCommands = (command: Command): readonly Command[] => {
|
|
207
|
+
const commands: Command[] = [];
|
|
208
|
+
let current: Command | null = command;
|
|
209
|
+
while (current !== null && current.parent !== null) {
|
|
210
|
+
commands.push(current);
|
|
211
|
+
current = current.parent;
|
|
212
|
+
}
|
|
213
|
+
return commands.toReversed();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const rootCommandFor = (command: Command): Command => {
|
|
217
|
+
let root = command;
|
|
218
|
+
while (root.parent !== null) {
|
|
219
|
+
root = root.parent;
|
|
220
|
+
}
|
|
221
|
+
return root;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const invocationArgsFor = (command: Command): readonly string[] =>
|
|
225
|
+
rootCommandFor(command).args;
|
|
226
|
+
|
|
227
|
+
const visibleOptionsFor = (command: Command): readonly Option[] => {
|
|
228
|
+
const commands: Command[] = [];
|
|
229
|
+
let current: Command | null = command;
|
|
230
|
+
while (current !== null) {
|
|
231
|
+
commands.push(current);
|
|
232
|
+
current = current.parent;
|
|
233
|
+
}
|
|
234
|
+
return commands.toReversed().flatMap((owner) => owner.options);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
interface InvocationOptionMatch {
|
|
238
|
+
readonly inlineValue: boolean;
|
|
239
|
+
readonly option: Option;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const findShortOption = (
|
|
243
|
+
options: readonly Option[],
|
|
244
|
+
short: string
|
|
245
|
+
): Option | undefined => options.find((candidate) => candidate.short === short);
|
|
246
|
+
|
|
247
|
+
const invocationOptionMatches = (
|
|
248
|
+
options: readonly Option[],
|
|
249
|
+
token: string
|
|
250
|
+
): readonly InvocationOptionMatch[] => {
|
|
251
|
+
const exact = options.filter(
|
|
252
|
+
(option) => token === option.long || token === option.short
|
|
253
|
+
);
|
|
254
|
+
if (exact.length > 0) {
|
|
255
|
+
return exact.map((option) => ({ inlineValue: false, option }));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const longWithValue = options.filter(
|
|
259
|
+
(option) =>
|
|
260
|
+
option.long !== undefined &&
|
|
261
|
+
(option.required || option.optional) &&
|
|
262
|
+
token.startsWith(`${option.long}=`)
|
|
263
|
+
);
|
|
264
|
+
if (longWithValue.length > 0) {
|
|
265
|
+
return longWithValue.map((option) => ({ inlineValue: true, option }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (token.length <= 2 || token[0] !== '-' || token[1] === '-') {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const matches: InvocationOptionMatch[] = [];
|
|
273
|
+
let group = token.slice(1);
|
|
274
|
+
while (group.length > 0) {
|
|
275
|
+
const option = findShortOption(options, `-${group[0]}`);
|
|
276
|
+
if (option === undefined) {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
const inlineValue =
|
|
280
|
+
(option.required || option.optional) && group.length > 1;
|
|
281
|
+
matches.push({ inlineValue, option });
|
|
282
|
+
if (option.required || option.optional) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
group = group.slice(1);
|
|
286
|
+
}
|
|
287
|
+
return matches;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const isNegativeNumberArg = (command: Command, token: string): boolean => {
|
|
291
|
+
if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(token)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (
|
|
296
|
+
let current: Command | null = command;
|
|
297
|
+
current !== null;
|
|
298
|
+
current = current.parent
|
|
299
|
+
) {
|
|
300
|
+
if (current.options.some((option) => /^-\d$/.test(option.short ?? ''))) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return true;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const isVariadicOptionValue = (
|
|
309
|
+
command: Command,
|
|
310
|
+
option: Option | undefined,
|
|
311
|
+
token: string
|
|
312
|
+
): boolean =>
|
|
313
|
+
option !== undefined &&
|
|
314
|
+
(!token.startsWith('-') || isNegativeNumberArg(command, token));
|
|
315
|
+
|
|
316
|
+
const optionConsumesFollowingValue = (
|
|
317
|
+
command: Command,
|
|
318
|
+
match: InvocationOptionMatch,
|
|
319
|
+
nextToken: string | undefined
|
|
320
|
+
): boolean =>
|
|
321
|
+
!match.inlineValue &&
|
|
322
|
+
(match.option.required ||
|
|
323
|
+
(match.option.optional &&
|
|
324
|
+
nextToken !== undefined &&
|
|
325
|
+
(!nextToken.startsWith('-') || isNegativeNumberArg(command, nextToken))));
|
|
326
|
+
|
|
327
|
+
interface InvocationScan {
|
|
328
|
+
readonly pathEnd: number | undefined;
|
|
329
|
+
readonly structuredInputIndexes: ReadonlySet<number>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const scanInvocation = (command: Command): InvocationScan => {
|
|
333
|
+
const rawArgs = invocationArgsFor(command);
|
|
334
|
+
const pathCommands = commandPathCommands(command);
|
|
335
|
+
const structuredInputIndexes = new Set<number>();
|
|
336
|
+
let activeCommand = rootCommandFor(command);
|
|
337
|
+
let activeVariadicOption: Option | undefined;
|
|
338
|
+
let optionsEnded = false;
|
|
339
|
+
let pathEnd: number | undefined;
|
|
340
|
+
let pathOffset = 0;
|
|
341
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
342
|
+
const token = rawArgs[index];
|
|
343
|
+
if (token === undefined) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (!optionsEnded && token === '--') {
|
|
347
|
+
optionsEnded = true;
|
|
348
|
+
activeVariadicOption = undefined;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (isVariadicOptionValue(command, activeVariadicOption, token)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
activeVariadicOption = undefined;
|
|
355
|
+
const optionMatches = optionsEnded
|
|
356
|
+
? []
|
|
357
|
+
: invocationOptionMatches(visibleOptionsFor(activeCommand), token);
|
|
358
|
+
const finalOptionMatch = optionMatches.at(-1);
|
|
359
|
+
if (finalOptionMatch !== undefined) {
|
|
360
|
+
if (
|
|
361
|
+
optionMatches.some((match) => structuredInputOptions.has(match.option))
|
|
362
|
+
) {
|
|
363
|
+
structuredInputIndexes.add(index);
|
|
364
|
+
}
|
|
365
|
+
activeVariadicOption =
|
|
366
|
+
finalOptionMatch.option.variadic && !finalOptionMatch.inlineValue
|
|
367
|
+
? finalOptionMatch.option
|
|
368
|
+
: undefined;
|
|
369
|
+
if (
|
|
370
|
+
optionConsumesFollowingValue(
|
|
371
|
+
command,
|
|
372
|
+
finalOptionMatch,
|
|
373
|
+
rawArgs[index + 1]
|
|
374
|
+
)
|
|
375
|
+
) {
|
|
376
|
+
index += 1;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const nextPathCommand = pathCommands[pathOffset];
|
|
381
|
+
if (nextPathCommand?.name() === token) {
|
|
382
|
+
activeCommand = nextPathCommand;
|
|
383
|
+
pathOffset += 1;
|
|
384
|
+
if (pathOffset === pathCommands.length) {
|
|
385
|
+
pathEnd = index;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { pathEnd, structuredInputIndexes };
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const hasStructuredInputAfterCommandPath = (command: Command): boolean => {
|
|
393
|
+
const { pathEnd, structuredInputIndexes } = scanInvocation(command);
|
|
394
|
+
if (pathEnd === undefined) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return [...structuredInputIndexes].some((index) => index > pathEnd);
|
|
398
|
+
};
|
|
201
399
|
|
|
202
400
|
const hasUserSuppliedOptionOutside = (
|
|
203
401
|
sourceCommand: Command,
|
|
@@ -217,6 +415,28 @@ const hasAnyPositionalValue = (
|
|
|
217
415
|
parsedArgs: Readonly<Record<string, unknown>>
|
|
218
416
|
): boolean => cmd.args.some((arg) => parsedArgs[arg.name] !== undefined);
|
|
219
417
|
|
|
418
|
+
const hasUserSuppliedStructuredInput = (
|
|
419
|
+
command: Command,
|
|
420
|
+
fallback?: BareChildFallback | undefined,
|
|
421
|
+
ownOptionsOnly = false
|
|
422
|
+
): boolean => {
|
|
423
|
+
for (const option of command.options) {
|
|
424
|
+
if (!structuredInputOptions.has(option)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const name = option.attributeName();
|
|
428
|
+
const source = ownOptionsOnly
|
|
429
|
+
? command.getOptionValueSource(name)
|
|
430
|
+
: command.getOptionValueSourceWithGlobals(name);
|
|
431
|
+
if (source !== undefined && source !== 'default' && source !== 'implied') {
|
|
432
|
+
return (
|
|
433
|
+
fallback === undefined || hasStructuredInputAfterCommandPath(command)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
};
|
|
439
|
+
|
|
220
440
|
const getActionTarget = (fallbackTarget: Command, actionArgs: unknown[]) => {
|
|
221
441
|
const candidate = actionArgs.at(-1);
|
|
222
442
|
return candidate instanceof Command ? candidate : fallbackTarget;
|
|
@@ -510,12 +730,15 @@ const maybeUseBareChildFallback = (
|
|
|
510
730
|
readonly parsedFlags: Record<string, unknown>;
|
|
511
731
|
readonly userSuppliedFlagKeys: ReadonlySet<string>;
|
|
512
732
|
} => {
|
|
513
|
-
const hasParentOnlySignal =
|
|
514
|
-
|
|
515
|
-
|
|
733
|
+
const hasParentOnlySignal =
|
|
734
|
+
fallback !== undefined &&
|
|
735
|
+
(hasUserSuppliedOptionOutside(fallback.parentTarget, target) ||
|
|
736
|
+
(hasUserSuppliedStructuredInput(fallback.parentTarget, undefined, true) &&
|
|
737
|
+
!hasStructuredInputAfterCommandPath(target)));
|
|
516
738
|
if (
|
|
517
739
|
!fallback ||
|
|
518
740
|
hasAnyPositionalValue(cmd, parsedArgs) ||
|
|
741
|
+
hasUserSuppliedStructuredInput(target, fallback) ||
|
|
519
742
|
hasUserSuppliedOptionOutside(target, fallback.parentTarget) ||
|
|
520
743
|
(fallback.requiresParentSignal && !hasParentOnlySignal)
|
|
521
744
|
) {
|