@ontrails/commander 1.0.0-beta.32 → 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 +41 -0
- package/package.json +3 -3
- package/src/surface.ts +8 -5
- package/src/to-commander.ts +232 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
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
|
+
|
|
16
|
+
## 1.0.0-beta.39
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- [`f42ca6e`](https://github.com/outfitter-dev/trails/commit/f42ca6e40b29155acec446e5bf44e52e014466bd): Hard cutover: the CLI consumes `cli` bindings from the app-authored surfaces overlay. Scalar bindings behave identically to the removed cliAliases (parity-tested) — the binding name splits on `.` into a transparent synonym command path for exactly one trail. List bindings arrive as command groups: each expanded member trail gets a group-prefixed route that dispatches the member trail with its identity preserved, and a singleton list stays a group. Expansion is fail-fast boundary validation: a scalar binding resolving to zero or multiple trails, or a group with an empty member union, is a `ValidationError` naming the binding. `DeriveTopoGraphOptions.cliAliases`, the `cliAliases`/`trailsCliAliases` app-module export convention, and the per-kind compile lift are deleted; `deriveCliCommands`/`createProgram` take `overlays` instead of `aliases`, and both topo-graph derivation pipelines expand the same bindings through one shared helper so runtime CLI routes and lock routes come from one semantic. A leftover legacy export is now a Warden error (`no-legacy-cli-alias-export`) naming the `surfaceOverlay({ cli: { ... } })` rewrite.
|
|
21
|
+
|
|
22
|
+
This is a breaking API removal shipped under the lockstep beta patch convention (pre-1.0 hard-cutover posture, zero external adoption); the removed options have no deprecation window by design.
|
|
23
|
+
|
|
24
|
+
## 1.0.0-beta.38
|
|
25
|
+
|
|
26
|
+
## 1.0.0-beta.37
|
|
27
|
+
|
|
28
|
+
## 1.0.0-beta.36
|
|
29
|
+
|
|
30
|
+
## 1.0.0-beta.35
|
|
31
|
+
|
|
32
|
+
## 1.0.0-beta.34
|
|
33
|
+
|
|
34
|
+
## 1.0.0-beta.33
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- [`945cb4b`](https://github.com/outfitter-dev/trails/commit/945cb4b0bef40ddb098c2a3816f9adad6f135410): Honor explicit structured input values over CLI flag defaults when commands merge
|
|
39
|
+
`--input-json` or `--input` payloads.
|
|
40
|
+
|
|
41
|
+
Commander now forwards user-supplied flag metadata so explicit flag values that
|
|
42
|
+
match a default still keep normal CLI precedence over structured input.
|
|
43
|
+
|
|
3
44
|
## 1.0.0-beta.32
|
|
4
45
|
|
|
5
46
|
### 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/surface.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
BaseSurfaceOptions,
|
|
7
|
-
CliCommandAliasInput,
|
|
8
7
|
Layer,
|
|
8
|
+
OverlayEnvelopeLike,
|
|
9
9
|
ResourceOverrideMap,
|
|
10
10
|
Topo,
|
|
11
11
|
TrailContextInit,
|
|
@@ -28,9 +28,6 @@ import { toCommander } from './to-commander.js';
|
|
|
28
28
|
* Options for creating Commander CLI surfaces from a Trails topo.
|
|
29
29
|
*/
|
|
30
30
|
export interface CreateProgramOptions extends BaseSurfaceOptions {
|
|
31
|
-
readonly aliases?:
|
|
32
|
-
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
33
|
-
| undefined;
|
|
34
31
|
readonly createContext?:
|
|
35
32
|
| (() => TrailContextInit | Promise<TrailContextInit>)
|
|
36
33
|
| undefined;
|
|
@@ -38,6 +35,12 @@ export interface CreateProgramOptions extends BaseSurfaceOptions {
|
|
|
38
35
|
readonly layers?: readonly Layer[] | undefined;
|
|
39
36
|
readonly name?: string | undefined;
|
|
40
37
|
readonly onResult?: ((ctx: ActionResultContext) => Promise<void>) | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* App-authored overlay envelopes (conventionally the app module's
|
|
40
|
+
* `trailsOverlays` export); the `surfaces` envelope's `cli` bindings
|
|
41
|
+
* project synonym and command-group routes onto the program.
|
|
42
|
+
*/
|
|
43
|
+
readonly overlays?: readonly OverlayEnvelopeLike[] | undefined;
|
|
41
44
|
readonly presets?: CliFlag[][] | undefined;
|
|
42
45
|
readonly resources?: ResourceOverrideMap | undefined;
|
|
43
46
|
readonly resolveInput?: InputResolver | undefined;
|
|
@@ -93,7 +96,6 @@ export const createProgram = (
|
|
|
93
96
|
options: CreateProgramOptions = {}
|
|
94
97
|
) => {
|
|
95
98
|
const commandsResult = deriveCliCommands(graph, {
|
|
96
|
-
aliases: options.aliases,
|
|
97
99
|
configValues: options.configValues,
|
|
98
100
|
createContext: options.createContext,
|
|
99
101
|
exclude: options.exclude,
|
|
@@ -101,6 +103,7 @@ export const createProgram = (
|
|
|
101
103
|
intent: options.intent,
|
|
102
104
|
layers: options.layers,
|
|
103
105
|
onResult: options.onResult ?? defaultOnResult,
|
|
106
|
+
overlays: options.overlays,
|
|
104
107
|
presets: options.presets,
|
|
105
108
|
resolveInput: options.resolveInput,
|
|
106
109
|
resolvePermitFromToken: options.resolvePermitFromToken,
|
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
|
) {
|
|
@@ -581,7 +804,12 @@ const wireAction = (
|
|
|
581
804
|
action.parsedFlags,
|
|
582
805
|
action.userSuppliedFlagKeys
|
|
583
806
|
);
|
|
584
|
-
await action.command.execute(action.parsedArgs, parsedFlags
|
|
807
|
+
await action.command.execute(action.parsedArgs, parsedFlags, undefined, {
|
|
808
|
+
userSuppliedFlagKeys: getCanonicalUserSuppliedFlagKeys(
|
|
809
|
+
action.command.flags,
|
|
810
|
+
action.userSuppliedFlagKeys
|
|
811
|
+
),
|
|
812
|
+
});
|
|
585
813
|
} catch (error: unknown) {
|
|
586
814
|
handleError(
|
|
587
815
|
error,
|