@ontrails/commander 1.0.0-beta.39 → 1.0.0-beta.42
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 +19 -0
- package/README.md +11 -0
- package/package.json +3 -3
- package/src/multiselect-argv.ts +182 -0
- package/src/to-commander.ts +150 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @ontrails/commander
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.42
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`454e935`](https://github.com/outfitter-dev/trails/commit/454e935088782a181df89a205c0ff6f2eb936434): Define bounded multiselect argv normalization in the framework CLI model and apply it automatically in the Commander adapter, accepting both contiguous and repeated forms while preserving child routes after the first explicit value.
|
|
8
|
+
|
|
9
|
+
## 1.0.0-beta.41
|
|
10
|
+
|
|
11
|
+
## 1.0.0-beta.40
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`4030698`](https://github.com/outfitter-dev/trails/commit/40306984467625844564f0f84156530d7118a79c): Keep structured input on nested child commands from being reinterpreted as a
|
|
16
|
+
bare child-name positional fallback, while preserving schema-authored
|
|
17
|
+
`inputJson` flags as ordinary trail input, including through the public Trails
|
|
18
|
+
CLI. Optional numeric flags now consume negative values with Commander's own
|
|
19
|
+
parsing semantics, and variadic flags consume every following value, before
|
|
20
|
+
nested command routing is resolved.
|
|
21
|
+
|
|
3
22
|
## 1.0.0-beta.39
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -35,6 +35,17 @@ if (commands.isErr()) {
|
|
|
35
35
|
const program = toCommander(commands.value, { name: 'myapp' });
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Multiselect flags
|
|
39
|
+
|
|
40
|
+
Schema fields such as `z.array(z.enum(['cli', 'mcp', 'http']))` derive a bounded multiselect flag. The shared CLI argv normalizer lets adapters accept both contiguous and repeated forms; Commander applies it automatically:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
myapp create --surfaces cli mcp http
|
|
44
|
+
myapp create --surfaces cli --surfaces mcp --surfaces http
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The first matching token after the flag is its explicit value. After that first value, additional collection stops before known child routes or values outside the declared choices. Adopters do not need custom parsing or surface configuration for either form.
|
|
48
|
+
|
|
38
49
|
## Installation
|
|
39
50
|
|
|
40
51
|
```bash
|
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.42",
|
|
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.42",
|
|
26
|
+
"@ontrails/core": "^1.0.0-beta.42",
|
|
27
27
|
"commander": "^14.0.3"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { ParseOptions, Option } from 'commander';
|
|
3
|
+
import { normalizeCliArgv } from '@ontrails/cli';
|
|
4
|
+
import type { CliCommand } from '@ontrails/cli';
|
|
5
|
+
|
|
6
|
+
type EffectiveParseOptions = Omit<ParseOptions, 'from'> & {
|
|
7
|
+
readonly from?: ParseOptions['from'] | 'eval' | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const visibleOptionsFor = (command: Command): readonly Option[] => {
|
|
11
|
+
const commands: Command[] = [];
|
|
12
|
+
let current: Command | null = command;
|
|
13
|
+
while (current !== null) {
|
|
14
|
+
commands.push(current);
|
|
15
|
+
current = current.parent;
|
|
16
|
+
}
|
|
17
|
+
return commands.toReversed().flatMap((owner) => owner.options);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface InvocationOptionMatch {
|
|
21
|
+
readonly inlineValue: boolean;
|
|
22
|
+
readonly option: Option;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const findShortOption = (
|
|
26
|
+
options: readonly Option[],
|
|
27
|
+
short: string
|
|
28
|
+
): Option | undefined => options.find((candidate) => candidate.short === short);
|
|
29
|
+
|
|
30
|
+
export const invocationOptionMatches = (
|
|
31
|
+
options: readonly Option[],
|
|
32
|
+
token: string
|
|
33
|
+
): readonly InvocationOptionMatch[] => {
|
|
34
|
+
const exact = options.filter(
|
|
35
|
+
(option) => token === option.long || token === option.short
|
|
36
|
+
);
|
|
37
|
+
if (exact.length > 0) {
|
|
38
|
+
return exact.map((option) => ({ inlineValue: false, option }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const longWithValue = options.filter(
|
|
42
|
+
(option) =>
|
|
43
|
+
option.long !== undefined &&
|
|
44
|
+
(option.required || option.optional) &&
|
|
45
|
+
token.startsWith(`${option.long}=`)
|
|
46
|
+
);
|
|
47
|
+
if (longWithValue.length > 0) {
|
|
48
|
+
return longWithValue.map((option) => ({ inlineValue: true, option }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (token.length <= 2 || token[0] !== '-' || token[1] === '-') {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const matches: InvocationOptionMatch[] = [];
|
|
56
|
+
let group = token.slice(1);
|
|
57
|
+
while (group.length > 0) {
|
|
58
|
+
const option = findShortOption(options, `-${group[0]}`);
|
|
59
|
+
if (option === undefined) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const inlineValue =
|
|
63
|
+
(option.required || option.optional) && group.length > 1;
|
|
64
|
+
matches.push({ inlineValue, option });
|
|
65
|
+
if (option.required || option.optional) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
group = group.slice(1);
|
|
69
|
+
}
|
|
70
|
+
return matches;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const isNegativeNumberArg = (
|
|
74
|
+
command: Command,
|
|
75
|
+
token: string
|
|
76
|
+
): boolean => {
|
|
77
|
+
if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(token)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (
|
|
82
|
+
let current: Command | null = command;
|
|
83
|
+
current !== null;
|
|
84
|
+
current = current.parent
|
|
85
|
+
) {
|
|
86
|
+
if (current.options.some((option) => /^-\d$/.test(option.short ?? ''))) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const optionConsumesFollowingValue = (
|
|
95
|
+
command: Command,
|
|
96
|
+
match: InvocationOptionMatch,
|
|
97
|
+
nextToken: string | undefined
|
|
98
|
+
): boolean =>
|
|
99
|
+
!match.inlineValue &&
|
|
100
|
+
(match.option.required ||
|
|
101
|
+
(match.option.optional &&
|
|
102
|
+
nextToken !== undefined &&
|
|
103
|
+
(!nextToken.startsWith('-') || isNegativeNumberArg(command, nextToken))));
|
|
104
|
+
|
|
105
|
+
const argvUserStart = (
|
|
106
|
+
parseOptions?: EffectiveParseOptions | undefined
|
|
107
|
+
): number => {
|
|
108
|
+
if (parseOptions?.from === 'user') {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (parseOptions?.from === 'eval') {
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
if (parseOptions?.from === 'electron') {
|
|
115
|
+
const electronProcess = process as NodeJS.Process & {
|
|
116
|
+
readonly defaultApp?: boolean | undefined;
|
|
117
|
+
};
|
|
118
|
+
return electronProcess.defaultApp ? 2 : 1;
|
|
119
|
+
}
|
|
120
|
+
return 2;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const effectiveParseOptions = (
|
|
124
|
+
argv: readonly string[] | undefined,
|
|
125
|
+
parseOptions: ParseOptions | undefined
|
|
126
|
+
): EffectiveParseOptions | undefined => {
|
|
127
|
+
if (
|
|
128
|
+
argv === undefined &&
|
|
129
|
+
parseOptions?.from === undefined &&
|
|
130
|
+
process.execArgv.some((arg) =>
|
|
131
|
+
['-e', '--eval', '-p', '--print'].includes(arg)
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
// Commander supports this origin internally but does not publish it in
|
|
135
|
+
// ParseOptions. Preserve its one-token offset while normalizing argv.
|
|
136
|
+
return { from: 'eval' };
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
argv === undefined &&
|
|
140
|
+
parseOptions?.from === undefined &&
|
|
141
|
+
process.versions['electron'] !== undefined
|
|
142
|
+
) {
|
|
143
|
+
return { from: 'electron' };
|
|
144
|
+
}
|
|
145
|
+
return parseOptions;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export class TrailsCommanderProgram extends Command {
|
|
149
|
+
readonly #commands: readonly CliCommand[];
|
|
150
|
+
|
|
151
|
+
constructor(commands: readonly CliCommand[]) {
|
|
152
|
+
super();
|
|
153
|
+
this.#commands = commands;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#normalizeArgv(
|
|
157
|
+
argv: readonly string[] | undefined,
|
|
158
|
+
parseOptions: EffectiveParseOptions | undefined
|
|
159
|
+
): readonly string[] {
|
|
160
|
+
const input = argv ?? process.argv;
|
|
161
|
+
const start = argvUserStart(parseOptions);
|
|
162
|
+
return [
|
|
163
|
+
...input.slice(0, start),
|
|
164
|
+
...normalizeCliArgv(this.#commands, input.slice(start)),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override parse(argv?: readonly string[], parseOptions?: ParseOptions): this {
|
|
169
|
+
const options = effectiveParseOptions(argv, parseOptions);
|
|
170
|
+
const normalized = this.#normalizeArgv(argv, options);
|
|
171
|
+
return super.parse(normalized, options as ParseOptions | undefined);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override parseAsync(
|
|
175
|
+
argv?: readonly string[],
|
|
176
|
+
parseOptions?: ParseOptions
|
|
177
|
+
): Promise<this> {
|
|
178
|
+
const options = effectiveParseOptions(argv, parseOptions);
|
|
179
|
+
const normalized = this.#normalizeArgv(argv, options);
|
|
180
|
+
return super.parseAsync(normalized, options as ParseOptions | undefined);
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/to-commander.ts
CHANGED
|
@@ -18,6 +18,14 @@ import {
|
|
|
18
18
|
} from '@ontrails/cli';
|
|
19
19
|
import { Command, InvalidArgumentError, Option } from 'commander';
|
|
20
20
|
|
|
21
|
+
import {
|
|
22
|
+
invocationOptionMatches,
|
|
23
|
+
isNegativeNumberArg,
|
|
24
|
+
optionConsumesFollowingValue,
|
|
25
|
+
TrailsCommanderProgram,
|
|
26
|
+
visibleOptionsFor,
|
|
27
|
+
} from './multiselect-argv.js';
|
|
28
|
+
|
|
21
29
|
// ---------------------------------------------------------------------------
|
|
22
30
|
// Options
|
|
23
31
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +92,8 @@ const buildRepeatableArrayParser =
|
|
|
84
92
|
return [...(Array.isArray(previous) ? previous : []), parsed];
|
|
85
93
|
};
|
|
86
94
|
|
|
95
|
+
const structuredInputOptions = new WeakSet<Option>();
|
|
96
|
+
|
|
87
97
|
/** Apply common modifiers (choices, default, arg parser) to a Commander Option. */
|
|
88
98
|
const applyOptionModifiers = (opt: Option, flag: CliFlag): void => {
|
|
89
99
|
if (isRepeatableArrayFlag(flag)) {
|
|
@@ -107,6 +117,9 @@ const applyOptionModifiers = (opt: Option, flag: CliFlag): void => {
|
|
|
107
117
|
/** Build Commander Option(s) from a CliFlag. Returns one or two options. */
|
|
108
118
|
const buildOptions = (flag: CliFlag): Option[] => {
|
|
109
119
|
const opt = new Option(buildFlagString(flag), flag.description);
|
|
120
|
+
if (flag.role === 'structured-input') {
|
|
121
|
+
structuredInputOptions.add(opt);
|
|
122
|
+
}
|
|
110
123
|
applyOptionModifiers(opt, flag);
|
|
111
124
|
const valueAliasOptions = (flag.valueAliases ?? []).map(
|
|
112
125
|
(alias) =>
|
|
@@ -198,6 +211,107 @@ const INHERITED_SURFACE_OPTION_KEYS = new Set([
|
|
|
198
211
|
'trace',
|
|
199
212
|
'watch',
|
|
200
213
|
]);
|
|
214
|
+
const commandPathCommands = (command: Command): readonly Command[] => {
|
|
215
|
+
const commands: Command[] = [];
|
|
216
|
+
let current: Command | null = command;
|
|
217
|
+
while (current !== null && current.parent !== null) {
|
|
218
|
+
commands.push(current);
|
|
219
|
+
current = current.parent;
|
|
220
|
+
}
|
|
221
|
+
return commands.toReversed();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const rootCommandFor = (command: Command): Command => {
|
|
225
|
+
let root = command;
|
|
226
|
+
while (root.parent !== null) {
|
|
227
|
+
root = root.parent;
|
|
228
|
+
}
|
|
229
|
+
return root;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const invocationArgsFor = (command: Command): readonly string[] =>
|
|
233
|
+
rootCommandFor(command).args;
|
|
234
|
+
|
|
235
|
+
const isVariadicOptionValue = (
|
|
236
|
+
command: Command,
|
|
237
|
+
option: Option | undefined,
|
|
238
|
+
token: string
|
|
239
|
+
): boolean =>
|
|
240
|
+
option !== undefined &&
|
|
241
|
+
(!token.startsWith('-') || isNegativeNumberArg(command, token));
|
|
242
|
+
|
|
243
|
+
interface InvocationScan {
|
|
244
|
+
readonly pathEnd: number | undefined;
|
|
245
|
+
readonly structuredInputIndexes: ReadonlySet<number>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const scanInvocation = (command: Command): InvocationScan => {
|
|
249
|
+
const rawArgs = invocationArgsFor(command);
|
|
250
|
+
const pathCommands = commandPathCommands(command);
|
|
251
|
+
const structuredInputIndexes = new Set<number>();
|
|
252
|
+
let activeCommand = rootCommandFor(command);
|
|
253
|
+
let activeVariadicOption: Option | undefined;
|
|
254
|
+
let optionsEnded = false;
|
|
255
|
+
let pathEnd: number | undefined;
|
|
256
|
+
let pathOffset = 0;
|
|
257
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
258
|
+
const token = rawArgs[index];
|
|
259
|
+
if (token === undefined) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!optionsEnded && token === '--') {
|
|
263
|
+
optionsEnded = true;
|
|
264
|
+
activeVariadicOption = undefined;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (isVariadicOptionValue(command, activeVariadicOption, token)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
activeVariadicOption = undefined;
|
|
271
|
+
const optionMatches = optionsEnded
|
|
272
|
+
? []
|
|
273
|
+
: invocationOptionMatches(visibleOptionsFor(activeCommand), token);
|
|
274
|
+
const finalOptionMatch = optionMatches.at(-1);
|
|
275
|
+
if (finalOptionMatch !== undefined) {
|
|
276
|
+
if (
|
|
277
|
+
optionMatches.some((match) => structuredInputOptions.has(match.option))
|
|
278
|
+
) {
|
|
279
|
+
structuredInputIndexes.add(index);
|
|
280
|
+
}
|
|
281
|
+
activeVariadicOption =
|
|
282
|
+
finalOptionMatch.option.variadic && !finalOptionMatch.inlineValue
|
|
283
|
+
? finalOptionMatch.option
|
|
284
|
+
: undefined;
|
|
285
|
+
if (
|
|
286
|
+
optionConsumesFollowingValue(
|
|
287
|
+
command,
|
|
288
|
+
finalOptionMatch,
|
|
289
|
+
rawArgs[index + 1]
|
|
290
|
+
)
|
|
291
|
+
) {
|
|
292
|
+
index += 1;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const nextPathCommand = pathCommands[pathOffset];
|
|
297
|
+
if (nextPathCommand?.name() === token) {
|
|
298
|
+
activeCommand = nextPathCommand;
|
|
299
|
+
pathOffset += 1;
|
|
300
|
+
if (pathOffset === pathCommands.length) {
|
|
301
|
+
pathEnd = index;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { pathEnd, structuredInputIndexes };
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const hasStructuredInputAfterCommandPath = (command: Command): boolean => {
|
|
309
|
+
const { pathEnd, structuredInputIndexes } = scanInvocation(command);
|
|
310
|
+
if (pathEnd === undefined) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return [...structuredInputIndexes].some((index) => index > pathEnd);
|
|
314
|
+
};
|
|
201
315
|
|
|
202
316
|
const hasUserSuppliedOptionOutside = (
|
|
203
317
|
sourceCommand: Command,
|
|
@@ -217,6 +331,28 @@ const hasAnyPositionalValue = (
|
|
|
217
331
|
parsedArgs: Readonly<Record<string, unknown>>
|
|
218
332
|
): boolean => cmd.args.some((arg) => parsedArgs[arg.name] !== undefined);
|
|
219
333
|
|
|
334
|
+
const hasUserSuppliedStructuredInput = (
|
|
335
|
+
command: Command,
|
|
336
|
+
fallback?: BareChildFallback | undefined,
|
|
337
|
+
ownOptionsOnly = false
|
|
338
|
+
): boolean => {
|
|
339
|
+
for (const option of command.options) {
|
|
340
|
+
if (!structuredInputOptions.has(option)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const name = option.attributeName();
|
|
344
|
+
const source = ownOptionsOnly
|
|
345
|
+
? command.getOptionValueSource(name)
|
|
346
|
+
: command.getOptionValueSourceWithGlobals(name);
|
|
347
|
+
if (source !== undefined && source !== 'default' && source !== 'implied') {
|
|
348
|
+
return (
|
|
349
|
+
fallback === undefined || hasStructuredInputAfterCommandPath(command)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
};
|
|
355
|
+
|
|
220
356
|
const getActionTarget = (fallbackTarget: Command, actionArgs: unknown[]) => {
|
|
221
357
|
const candidate = actionArgs.at(-1);
|
|
222
358
|
return candidate instanceof Command ? candidate : fallbackTarget;
|
|
@@ -286,8 +422,13 @@ const getUserSuppliedFlagKeys = (
|
|
|
286
422
|
): ReadonlySet<string> => {
|
|
287
423
|
const userSupplied = new Set<string>();
|
|
288
424
|
for (const key of getFlagOptionKeys(flags)) {
|
|
289
|
-
|
|
290
|
-
|
|
425
|
+
let owner: Command | null = command;
|
|
426
|
+
while (owner !== null) {
|
|
427
|
+
if (isUserSuppliedOption(owner, key)) {
|
|
428
|
+
userSupplied.add(key);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
owner = owner.parent;
|
|
291
432
|
}
|
|
292
433
|
}
|
|
293
434
|
return userSupplied;
|
|
@@ -510,12 +651,15 @@ const maybeUseBareChildFallback = (
|
|
|
510
651
|
readonly parsedFlags: Record<string, unknown>;
|
|
511
652
|
readonly userSuppliedFlagKeys: ReadonlySet<string>;
|
|
512
653
|
} => {
|
|
513
|
-
const hasParentOnlySignal =
|
|
514
|
-
|
|
515
|
-
|
|
654
|
+
const hasParentOnlySignal =
|
|
655
|
+
fallback !== undefined &&
|
|
656
|
+
(hasUserSuppliedOptionOutside(fallback.parentTarget, target) ||
|
|
657
|
+
(hasUserSuppliedStructuredInput(fallback.parentTarget, undefined, true) &&
|
|
658
|
+
!hasStructuredInputAfterCommandPath(target)));
|
|
516
659
|
if (
|
|
517
660
|
!fallback ||
|
|
518
661
|
hasAnyPositionalValue(cmd, parsedArgs) ||
|
|
662
|
+
hasUserSuppliedStructuredInput(target, fallback) ||
|
|
519
663
|
hasUserSuppliedOptionOutside(target, fallback.parentTarget) ||
|
|
520
664
|
(fallback.requiresParentSignal && !hasParentOnlySignal)
|
|
521
665
|
) {
|
|
@@ -776,7 +920,7 @@ export const toCommander = (
|
|
|
776
920
|
options?: ToCommanderOptions
|
|
777
921
|
): Command => {
|
|
778
922
|
validateCliCommands(commands);
|
|
779
|
-
const program = new
|
|
923
|
+
const program = new TrailsCommanderProgram(commands);
|
|
780
924
|
applyOptions(program, options);
|
|
781
925
|
const topoName = options?.topoName ?? options?.name ?? program.name();
|
|
782
926
|
const nodes = new Map<string, CommandNodeState>();
|