@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 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.39",
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.39",
26
- "@ontrails/core": "^1.0.0-beta.39",
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": {
@@ -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 = fallback
514
- ? hasUserSuppliedOptionOutside(fallback.parentTarget, target)
515
- : false;
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
  ) {