@flowscripter/dynamic-cli-framework 1.0.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/.github/workflows/check-bun-dependencies.yml +9 -0
- package/.github/workflows/lint-pr-message.yml +11 -0
- package/.github/workflows/release-bun-library.yml +8 -0
- package/.github/workflows/validate-bun-library-pr.yml +8 -0
- package/LICENSE +21 -0
- package/README.md +1475 -0
- package/bun.lock +63 -0
- package/index.ts +89 -0
- package/package.json +23 -0
- package/src/api/CLI.ts +17 -0
- package/src/api/CLIConfig.ts +19 -0
- package/src/api/Context.ts +25 -0
- package/src/api/RunResult.ts +142 -0
- package/src/api/argument/Argument.ts +36 -0
- package/src/api/argument/ArgumentValueTypes.ts +87 -0
- package/src/api/argument/ComplexOption.ts +31 -0
- package/src/api/argument/GlobalCommandArgument.ts +17 -0
- package/src/api/argument/Option.ts +32 -0
- package/src/api/argument/Positional.ts +27 -0
- package/src/api/argument/SubCommandArgument.ts +20 -0
- package/src/api/command/Command.ts +22 -0
- package/src/api/command/GlobalCommand.ts +33 -0
- package/src/api/command/GlobalModifierCommand.ts +12 -0
- package/src/api/command/GroupCommand.ts +20 -0
- package/src/api/command/SubCommand.ts +41 -0
- package/src/api/command/UsageExample.ts +21 -0
- package/src/api/service/ServiceProvider.ts +52 -0
- package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
- package/src/api/service/core/KeyValueService.ts +28 -0
- package/src/api/service/core/PrinterService.ts +266 -0
- package/src/api/service/core/ShutdownService.ts +12 -0
- package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
- package/src/cli/BaseCLI.ts +312 -0
- package/src/cli/DefaultRuntimeCLI.ts +54 -0
- package/src/command/MultiCommandCliHelpCommand.ts +394 -0
- package/src/command/SingleCommandCliHelpCommand.ts +183 -0
- package/src/command/UsageCommand.ts +35 -0
- package/src/command/VersionCommand.ts +22 -0
- package/src/launcher.ts +127 -0
- package/src/runtime/DefaultContext.ts +32 -0
- package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
- package/src/runtime/command/CommandTypeGuards.ts +30 -0
- package/src/runtime/command/CommandValidator.ts +549 -0
- package/src/runtime/parser.ts +219 -0
- package/src/runtime/registry/CommandRegistry.ts +121 -0
- package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
- package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
- package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
- package/src/runtime/runner.ts +697 -0
- package/src/runtime/scanner.ts +417 -0
- package/src/runtime/values/ValuePopulationResult.ts +41 -0
- package/src/runtime/values/argumentValueMerge.ts +201 -0
- package/src/runtime/values/argumentValueValidation.ts +610 -0
- package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
- package/src/runtime/values/subCommandValuePopulation.ts +680 -0
- package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
- package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
- package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
- package/src/service/banner/BannerServiceProvider.ts +99 -0
- package/src/service/banner/command/NoBannerCommand.ts +42 -0
- package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
- package/src/service/configuration/DefaultKeyValueService.ts +60 -0
- package/src/service/configuration/command/ConfigCommand.ts +43 -0
- package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
- package/src/service/printer/DefaultPrinterService.ts +256 -0
- package/src/service/printer/PrinterServiceProvider.ts +61 -0
- package/src/service/printer/command/DarkModeCommand.ts +43 -0
- package/src/service/printer/command/LogLevelCommand.ts +68 -0
- package/src/service/printer/command/NoColorCommand.ts +43 -0
- package/src/service/printer/terminal/Ansi.ts +10 -0
- package/src/service/printer/terminal/Color.ts +14 -0
- package/src/service/printer/terminal/Progress.ts +373 -0
- package/src/service/printer/terminal/Spinner.ts +100 -0
- package/src/service/printer/terminal/Styler.ts +5 -0
- package/src/service/printer/terminal/Terminal.ts +8 -0
- package/src/service/printer/terminal/Theme.ts +34 -0
- package/src/service/printer/terminal/TtyStyler.ts +39 -0
- package/src/service/printer/terminal/TtyTerminal.ts +65 -0
- package/src/service/shutdown/DefaultShutdownService.ts +9 -0
- package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
- package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
- package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
- package/src/util/configHelper.ts +95 -0
- package/src/util/envVarHelper.ts +338 -0
- package/src/util/helpHelper.ts +751 -0
- package/src/util/logger.ts +84 -0
- package/src/util/runnerHelper.ts +112 -0
- package/tests/cli/BaseCLI.test.ts +344 -0
- package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
- package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
- package/tests/command/UsageCommand.test.ts +53 -0
- package/tests/command/VersionCommand.test.ts +36 -0
- package/tests/fixtures/CLIConfig.ts +9 -0
- package/tests/fixtures/Command.ts +229 -0
- package/tests/fixtures/CommandRegistry.ts +8 -0
- package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
- package/tests/fixtures/Context.ts +33 -0
- package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
- package/tests/fixtures/StreamString.ts +37 -0
- package/tests/fixtures/util.ts +41 -0
- package/tests/runtime/DefaultContext.test.ts +20 -0
- package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
- package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
- package/tests/runtime/command/CommandValidator.test.ts +816 -0
- package/tests/runtime/parser.test.ts +766 -0
- package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
- package/tests/runtime/runner.test.ts +1298 -0
- package/tests/runtime/scanner.test.ts +1113 -0
- package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
- package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
- package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
- package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
- package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
- package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
- package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
- package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
- package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
- package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
- package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
- package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
- package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
- package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
- package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
- package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
- package/tests/service/printer/terminal/Progress.test.ts +40 -0
- package/tests/service/printer/terminal/Spinner.test.ts +57 -0
- package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
- package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
- package/tests/util/configHelper.test.ts +275 -0
- package/tests/util/envVarHelper.test.ts +318 -0
- package/tests/util/logger.test.ts +10 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import type Context from "../api/Context.ts";
|
|
2
|
+
import type RunResult from "../api/RunResult.ts";
|
|
3
|
+
import { RunState } from "../api/RunResult.ts";
|
|
4
|
+
import type CommandRegistry from "./registry/CommandRegistry.ts";
|
|
5
|
+
import { isGlobalCommand, isSubCommand } from "./command/CommandTypeGuards.ts";
|
|
6
|
+
import type {
|
|
7
|
+
ArgumentSingleValueType,
|
|
8
|
+
ArgumentValues,
|
|
9
|
+
PopulatedArgumentSingleValueType,
|
|
10
|
+
PopulatedArgumentValues,
|
|
11
|
+
} from "../api/argument/ArgumentValueTypes.ts";
|
|
12
|
+
import getLogger from "../util/logger.ts";
|
|
13
|
+
import {
|
|
14
|
+
printCommandExecutionError,
|
|
15
|
+
printNoCommandSpecifiedError,
|
|
16
|
+
printParseResultError,
|
|
17
|
+
printUnusedArgsWarning,
|
|
18
|
+
} from "../util/runnerHelper.ts";
|
|
19
|
+
import {
|
|
20
|
+
scanForGlobalModifierCommandClauses,
|
|
21
|
+
scanForNonModifierCommandClause,
|
|
22
|
+
} from "./scanner.ts";
|
|
23
|
+
import type ServiceProviderRegistry from "./registry/ServiceProviderRegistry.ts";
|
|
24
|
+
import {
|
|
25
|
+
parseGlobalCommandClause,
|
|
26
|
+
type ParseResult,
|
|
27
|
+
parseSubCommandClause,
|
|
28
|
+
} from "./parser.ts";
|
|
29
|
+
import type ConfigurationServiceProvider from "../service/configuration/ConfigurationServiceProvider.ts";
|
|
30
|
+
import type GlobalModifierCommand from "../api/command/GlobalModifierCommand.ts";
|
|
31
|
+
import type GroupCommand from "../api/command/GroupCommand.ts";
|
|
32
|
+
import type SubCommand from "../api/command/SubCommand.ts";
|
|
33
|
+
import type GlobalCommand from "../api/command/GlobalCommand.ts";
|
|
34
|
+
import type Command from "../api/command/Command.ts";
|
|
35
|
+
|
|
36
|
+
const logger = getLogger("runner");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute the {@link Command} with arguments contained in the provided {@link ParseResult}.
|
|
40
|
+
*
|
|
41
|
+
* @param parseResult the {@link ParseResult} to execute.
|
|
42
|
+
* @param context the {@link Context} to use.
|
|
43
|
+
* @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
|
|
44
|
+
* @param isDefaultCommand whether the command is the default command for the CLI.
|
|
45
|
+
*/
|
|
46
|
+
async function executeParsedCommand(
|
|
47
|
+
parseResult: ParseResult,
|
|
48
|
+
context: Context,
|
|
49
|
+
configurationServiceProvider: ConfigurationServiceProvider | undefined,
|
|
50
|
+
isDefaultCommand = false,
|
|
51
|
+
): Promise<RunResult> {
|
|
52
|
+
try {
|
|
53
|
+
if (parseResult.groupCommand !== undefined) {
|
|
54
|
+
logger.debug(
|
|
55
|
+
"Executing group command with name: %s",
|
|
56
|
+
parseResult!.groupCommand!.name,
|
|
57
|
+
);
|
|
58
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
59
|
+
configurationServiceProvider.setCommandKeyValueScope(
|
|
60
|
+
parseResult!.groupCommand!.name,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
await parseResult.groupCommand.execute(context);
|
|
64
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
65
|
+
await configurationServiceProvider.clearKeyValueScope();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
logger.debug(
|
|
70
|
+
"Executing command with name: %s and args: %j",
|
|
71
|
+
parseResult!.command.name,
|
|
72
|
+
parseResult!.populatedArgumentValues,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
76
|
+
configurationServiceProvider.setCommandKeyValueScope(
|
|
77
|
+
parseResult.command.name,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (isSubCommand(parseResult.command)) {
|
|
81
|
+
await parseResult.command.execute(
|
|
82
|
+
context,
|
|
83
|
+
parseResult.populatedArgumentValues as ArgumentValues,
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
await (parseResult.command as GlobalCommand).execute(
|
|
87
|
+
context,
|
|
88
|
+
parseResult.populatedArgumentValues as ArgumentSingleValueType,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
92
|
+
await configurationServiceProvider.clearKeyValueScope();
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
await printCommandExecutionError(
|
|
96
|
+
context,
|
|
97
|
+
parseResult,
|
|
98
|
+
err as Error,
|
|
99
|
+
isDefaultCommand,
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
runState: RunState.EXECUTION_ERROR,
|
|
103
|
+
command: parseResult.command,
|
|
104
|
+
error: err as Error,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
runState: RunState.SUCCESS,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse arguments and execute any of the specified {@link GlobalModifierCommand} instances with specified argument values.
|
|
115
|
+
*
|
|
116
|
+
* @param availableArgSequences the list of argument sequences to process.
|
|
117
|
+
* @param unusedArgSequences an array to which any unused argument sequences will be added.
|
|
118
|
+
* @param globalModifierCommandsByName a map of non-modifier {@link Command} instances by name to parse for.
|
|
119
|
+
* @param globalModifierCommandsByShortAlias a map of {@link GlobalModifierCommand} instances by short alias to use when scanning.
|
|
120
|
+
* @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
|
|
121
|
+
* @param context the {@link Context} to use.
|
|
122
|
+
*/
|
|
123
|
+
async function findAndExecuteGlobalModifierCommands(
|
|
124
|
+
availableArgSequences: ReadonlyArray<ReadonlyArray<string>>,
|
|
125
|
+
unusedArgSequences: Array<ReadonlyArray<string>>,
|
|
126
|
+
globalModifierCommandsByName: ReadonlyMap<string, GlobalModifierCommand>,
|
|
127
|
+
globalModifierCommandsByShortAlias: ReadonlyMap<
|
|
128
|
+
string,
|
|
129
|
+
GlobalModifierCommand
|
|
130
|
+
>,
|
|
131
|
+
configurationServiceProvider: ConfigurationServiceProvider | undefined,
|
|
132
|
+
context: Context,
|
|
133
|
+
): Promise<RunResult | undefined> {
|
|
134
|
+
// build a list of GlobalModifierCommands to execute
|
|
135
|
+
const globalModifierCommandParseResults: Array<ParseResult> = [];
|
|
136
|
+
|
|
137
|
+
// maintain a list of remaining GlobalModifierCommands to scan
|
|
138
|
+
let remainingGlobalModifierCommands = Array.from(
|
|
139
|
+
globalModifierCommandsByName.values(),
|
|
140
|
+
);
|
|
141
|
+
const scanResult = scanForGlobalModifierCommandClauses(
|
|
142
|
+
availableArgSequences,
|
|
143
|
+
globalModifierCommandsByName,
|
|
144
|
+
globalModifierCommandsByShortAlias,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// add any unused args back to the list of still available args for next scan operation
|
|
148
|
+
unusedArgSequences.push(...scanResult.unusedArgSequences);
|
|
149
|
+
|
|
150
|
+
if (scanResult.globalModifierCommandClauses) {
|
|
151
|
+
for (
|
|
152
|
+
const globalModifierCommandClause of scanResult
|
|
153
|
+
.globalModifierCommandClauses
|
|
154
|
+
) {
|
|
155
|
+
// parse and fast fail on error
|
|
156
|
+
const defaultArgumentValues = configurationServiceProvider
|
|
157
|
+
? configurationServiceProvider
|
|
158
|
+
.getDefaultArgumentValues(
|
|
159
|
+
context.cliConfig,
|
|
160
|
+
globalModifierCommandClause.command,
|
|
161
|
+
)
|
|
162
|
+
: undefined;
|
|
163
|
+
const parseResult = parseGlobalCommandClause(
|
|
164
|
+
globalModifierCommandClause,
|
|
165
|
+
defaultArgumentValues as PopulatedArgumentSingleValueType,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
169
|
+
await printParseResultError(context, parseResult);
|
|
170
|
+
return {
|
|
171
|
+
runState: RunState.PARSE_ERROR,
|
|
172
|
+
command: parseResult.command,
|
|
173
|
+
invalidArguments: parseResult.invalidArguments,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// add any unused args back to the list of still available args for next scan operation
|
|
178
|
+
if (parseResult.unusedArgs.length > 0) {
|
|
179
|
+
unusedArgSequences.push(parseResult.unusedArgs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// add the clause to the list of clauses to execute
|
|
183
|
+
globalModifierCommandParseResults.push(parseResult);
|
|
184
|
+
|
|
185
|
+
// remove current GlobalModifierCommand from the list which we will later continue to
|
|
186
|
+
// scan for as a default command in the ConfigurationServiceProvider
|
|
187
|
+
remainingGlobalModifierCommands = remainingGlobalModifierCommands.filter((
|
|
188
|
+
command,
|
|
189
|
+
) => command.name !== globalModifierCommandClause.command.name);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// scan for remaining GlobalModifierCommand with default argument values in the ConfigurationServiceProvider
|
|
194
|
+
for (const globalModifierCommand of remainingGlobalModifierCommands) {
|
|
195
|
+
// if there is a config entry run this by default even though no arguments were provided on the command line
|
|
196
|
+
const defaultArgumentValues = configurationServiceProvider
|
|
197
|
+
? configurationServiceProvider
|
|
198
|
+
.getDefaultArgumentValues(context.cliConfig, globalModifierCommand)
|
|
199
|
+
: undefined;
|
|
200
|
+
|
|
201
|
+
if (defaultArgumentValues !== undefined) {
|
|
202
|
+
// parse and fast fail on error
|
|
203
|
+
const parseResult = parseGlobalCommandClause({
|
|
204
|
+
command: globalModifierCommand,
|
|
205
|
+
potentialArgs: [],
|
|
206
|
+
}, defaultArgumentValues as PopulatedArgumentSingleValueType);
|
|
207
|
+
|
|
208
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
209
|
+
await printParseResultError(context, parseResult);
|
|
210
|
+
return {
|
|
211
|
+
runState: RunState.PARSE_ERROR,
|
|
212
|
+
command: parseResult.command,
|
|
213
|
+
invalidArguments: parseResult.invalidArguments,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// add the clause to the list of clauses to execute
|
|
218
|
+
globalModifierCommandParseResults.push(parseResult);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// execute GlobalModifierCommand clauses in executePriority order
|
|
223
|
+
globalModifierCommandParseResults.sort((a, b) =>
|
|
224
|
+
(b.command as GlobalModifierCommand).executePriority -
|
|
225
|
+
(a.command as GlobalModifierCommand).executePriority
|
|
226
|
+
);
|
|
227
|
+
for (const parseResult of globalModifierCommandParseResults) {
|
|
228
|
+
// execute and fast fail on error
|
|
229
|
+
const runResult = await executeParsedCommand(
|
|
230
|
+
parseResult,
|
|
231
|
+
context,
|
|
232
|
+
configurationServiceProvider,
|
|
233
|
+
);
|
|
234
|
+
if (runResult.runState !== RunState.SUCCESS) {
|
|
235
|
+
return runResult;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Parse arguments and execute at most one of the specified non-modifier {@link Command} instances with specified argument values.
|
|
242
|
+
*
|
|
243
|
+
* @param availableArgs the list of argument sequences to process.
|
|
244
|
+
* @param unusedArgs an array to which any unused argument sequences will be added.
|
|
245
|
+
* @param nonModifierCommandsByName the map of non-modifier {@link Command} instances by name to parse for.
|
|
246
|
+
* @param globalCommandsByShortAlias the map of {@link GlobalCommand} instances by short alias to use when scanning.
|
|
247
|
+
* @param groupAndMemberCommandsByJoinedName optional map of {@link GroupCommand} and member {@link SubCommand} instances to use when scanning.
|
|
248
|
+
* @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
|
|
249
|
+
* @param context the {@link Context} to use.
|
|
250
|
+
*/
|
|
251
|
+
async function findAndExecuteNonModifierCommand(
|
|
252
|
+
availableArgs: ReadonlyArray<ReadonlyArray<string>>,
|
|
253
|
+
unusedArgs: Array<ReadonlyArray<string>>,
|
|
254
|
+
nonModifierCommandsByName: ReadonlyMap<string, Command>,
|
|
255
|
+
globalCommandsByShortAlias: ReadonlyMap<string, GlobalCommand>,
|
|
256
|
+
groupAndMemberCommandsByJoinedName:
|
|
257
|
+
| ReadonlyMap<string, { groupCommand: GroupCommand; command: SubCommand }>
|
|
258
|
+
| undefined,
|
|
259
|
+
configurationServiceProvider: ConfigurationServiceProvider | undefined,
|
|
260
|
+
context: Context,
|
|
261
|
+
): Promise<RunResult | undefined> {
|
|
262
|
+
let parseResult: ParseResult | undefined;
|
|
263
|
+
|
|
264
|
+
const scanResult = scanForNonModifierCommandClause(
|
|
265
|
+
availableArgs,
|
|
266
|
+
nonModifierCommandsByName,
|
|
267
|
+
globalCommandsByShortAlias,
|
|
268
|
+
groupAndMemberCommandsByJoinedName,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// add any unused args back to the list of still available args for next scan operation
|
|
272
|
+
unusedArgs.push(...scanResult.unusedArgSequences);
|
|
273
|
+
|
|
274
|
+
if (scanResult.nonModifierCommandClause) {
|
|
275
|
+
const defaultArgumentValues = configurationServiceProvider
|
|
276
|
+
? configurationServiceProvider
|
|
277
|
+
.getDefaultArgumentValues(
|
|
278
|
+
context.cliConfig,
|
|
279
|
+
scanResult.nonModifierCommandClause.command,
|
|
280
|
+
)
|
|
281
|
+
: undefined;
|
|
282
|
+
|
|
283
|
+
if (isGlobalCommand(scanResult.nonModifierCommandClause.command)) {
|
|
284
|
+
parseResult = parseGlobalCommandClause(
|
|
285
|
+
scanResult.nonModifierCommandClause,
|
|
286
|
+
defaultArgumentValues as PopulatedArgumentSingleValueType,
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
parseResult = parseSubCommandClause(
|
|
290
|
+
scanResult.nonModifierCommandClause,
|
|
291
|
+
defaultArgumentValues as PopulatedArgumentValues,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
296
|
+
await printParseResultError(context, parseResult);
|
|
297
|
+
return {
|
|
298
|
+
runState: RunState.PARSE_ERROR,
|
|
299
|
+
command: parseResult.command,
|
|
300
|
+
invalidArguments: parseResult.invalidArguments,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// add any unused args back to the list of still unused args
|
|
305
|
+
if (parseResult.unusedArgs.length > 0) {
|
|
306
|
+
unusedArgs.push(parseResult.unusedArgs);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// scan for a non-modifier Command with default argument values in the ConfigurationServiceProvider
|
|
310
|
+
for (const nonModifierCommand of nonModifierCommandsByName.values()) {
|
|
311
|
+
// if there is a config entry run this by default even though no arguments were provided on the command line
|
|
312
|
+
const defaultArgumentValues = configurationServiceProvider
|
|
313
|
+
? configurationServiceProvider
|
|
314
|
+
.getDefaultArgumentValues(context.cliConfig, nonModifierCommand)
|
|
315
|
+
: undefined;
|
|
316
|
+
|
|
317
|
+
if (defaultArgumentValues !== undefined) {
|
|
318
|
+
if (isGlobalCommand(nonModifierCommand)) {
|
|
319
|
+
parseResult = parseGlobalCommandClause({
|
|
320
|
+
command: nonModifierCommand,
|
|
321
|
+
potentialArgs: [],
|
|
322
|
+
}, defaultArgumentValues as PopulatedArgumentSingleValueType);
|
|
323
|
+
} else {
|
|
324
|
+
parseResult = parseSubCommandClause({
|
|
325
|
+
command: nonModifierCommand,
|
|
326
|
+
potentialArgs: [],
|
|
327
|
+
}, defaultArgumentValues as PopulatedArgumentValues);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
331
|
+
await printParseResultError(context, parseResult);
|
|
332
|
+
return {
|
|
333
|
+
runState: RunState.PARSE_ERROR,
|
|
334
|
+
command: parseResult.command,
|
|
335
|
+
invalidArguments: parseResult.invalidArguments,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// give up if still nothing found
|
|
345
|
+
if (parseResult === undefined) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// warn on unused args
|
|
350
|
+
if (unusedArgs.length > 0) {
|
|
351
|
+
await printUnusedArgsWarning(context, unusedArgs.flat());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return executeParsedCommand(
|
|
355
|
+
parseResult,
|
|
356
|
+
context,
|
|
357
|
+
configurationServiceProvider,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Assume provided command in use and parse arguments and execute with specified argument values if valid.
|
|
363
|
+
*
|
|
364
|
+
* @param availableArgs the list of argument sequences to process.
|
|
365
|
+
* @param unusedArgs an array to which any unused argument sequences will be added.
|
|
366
|
+
* @param defaultNonModifierCommand the default non-modifier {@link Command} to use.
|
|
367
|
+
* @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
|
|
368
|
+
* @param context the {@link Context} to use.
|
|
369
|
+
*/
|
|
370
|
+
async function findAndExecuteDefaultNonModifierCommand(
|
|
371
|
+
availableArgs: ReadonlyArray<ReadonlyArray<string>>,
|
|
372
|
+
unusedArgs: Array<ReadonlyArray<string>>,
|
|
373
|
+
defaultNonModifierCommand: Command,
|
|
374
|
+
configurationServiceProvider: ConfigurationServiceProvider | undefined,
|
|
375
|
+
context: Context,
|
|
376
|
+
): Promise<RunResult | undefined> {
|
|
377
|
+
const defaultArgumentValues = configurationServiceProvider
|
|
378
|
+
? configurationServiceProvider
|
|
379
|
+
.getDefaultArgumentValues(context.cliConfig, defaultNonModifierCommand)
|
|
380
|
+
: undefined;
|
|
381
|
+
const isGlobal = isGlobalCommand(defaultNonModifierCommand);
|
|
382
|
+
let parseResult: ParseResult | undefined;
|
|
383
|
+
|
|
384
|
+
// save invalid args to report them if no valid command is found
|
|
385
|
+
let lastErroneousParseResult;
|
|
386
|
+
|
|
387
|
+
// create a dummy clause using the default command for each arg sequence
|
|
388
|
+
for (const argSequence of availableArgs) {
|
|
389
|
+
// if already successfully parsed a clause, cycle through remaining arg sequences to add them to unusedArgs
|
|
390
|
+
if (parseResult) {
|
|
391
|
+
unusedArgs.push(argSequence);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (isGlobal) {
|
|
395
|
+
parseResult = parseGlobalCommandClause({
|
|
396
|
+
command: defaultNonModifierCommand,
|
|
397
|
+
potentialArgs: [...argSequence],
|
|
398
|
+
}, defaultArgumentValues as PopulatedArgumentSingleValueType);
|
|
399
|
+
} else {
|
|
400
|
+
parseResult = parseSubCommandClause({
|
|
401
|
+
command: defaultNonModifierCommand,
|
|
402
|
+
potentialArgs: [...argSequence],
|
|
403
|
+
}, defaultArgumentValues as PopulatedArgumentValues);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// add any unused args back to the list of still unused args
|
|
407
|
+
if (parseResult.unusedArgs.length > 0) {
|
|
408
|
+
unusedArgs.push(parseResult.unusedArgs);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
412
|
+
lastErroneousParseResult = parseResult;
|
|
413
|
+
parseResult = undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// even though no arguments were provided on the command line, run the command by default
|
|
418
|
+
// if there are no arguments for the command or there is a config entry
|
|
419
|
+
let commandHasNoArgs;
|
|
420
|
+
if (isGlobal) {
|
|
421
|
+
commandHasNoArgs =
|
|
422
|
+
(defaultNonModifierCommand as GlobalCommand).argument === undefined;
|
|
423
|
+
} else {
|
|
424
|
+
commandHasNoArgs =
|
|
425
|
+
((defaultNonModifierCommand as SubCommand).options.length === 0) &&
|
|
426
|
+
((defaultNonModifierCommand as SubCommand).positionals.length === 0);
|
|
427
|
+
}
|
|
428
|
+
if (
|
|
429
|
+
(parseResult === undefined) &&
|
|
430
|
+
(commandHasNoArgs || (defaultArgumentValues !== undefined))
|
|
431
|
+
) {
|
|
432
|
+
if (isGlobal) {
|
|
433
|
+
parseResult = parseGlobalCommandClause({
|
|
434
|
+
command: defaultNonModifierCommand,
|
|
435
|
+
potentialArgs: [],
|
|
436
|
+
}, defaultArgumentValues as PopulatedArgumentSingleValueType);
|
|
437
|
+
} else {
|
|
438
|
+
parseResult = parseSubCommandClause({
|
|
439
|
+
command: defaultNonModifierCommand,
|
|
440
|
+
potentialArgs: [],
|
|
441
|
+
}, defaultArgumentValues as PopulatedArgumentValues);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (parseResult.invalidArguments.length > 0) {
|
|
445
|
+
lastErroneousParseResult = parseResult;
|
|
446
|
+
parseResult = undefined;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// if there were invalid arguments, treat this as the error
|
|
451
|
+
if (lastErroneousParseResult) {
|
|
452
|
+
await printParseResultError(context, lastErroneousParseResult, true);
|
|
453
|
+
return {
|
|
454
|
+
runState: RunState.PARSE_ERROR,
|
|
455
|
+
command: defaultNonModifierCommand,
|
|
456
|
+
invalidArguments: lastErroneousParseResult.invalidArguments,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// give up if still nothing found
|
|
461
|
+
if (parseResult === undefined) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// warn on unused args
|
|
466
|
+
if (unusedArgs.length > 0) {
|
|
467
|
+
await printUnusedArgsWarning(context, unusedArgs.flat());
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return executeParsedCommand(
|
|
471
|
+
parseResult,
|
|
472
|
+
context,
|
|
473
|
+
configurationServiceProvider,
|
|
474
|
+
true,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Scan arguments and execute the parsed {@link Command} clauses with provided argument values
|
|
480
|
+
* using the provided {@link Context}.
|
|
481
|
+
*
|
|
482
|
+
* An overview of the logic (assuming the optional ConfigurationServiceProvider is provided):
|
|
483
|
+
*
|
|
484
|
+
* 1. For each ServiceProvider in servicePriority order:
|
|
485
|
+
* - Scan arguments for GlobalModifierCommand clauses provided by the ServiceProvider
|
|
486
|
+
* - For each discovered clause:
|
|
487
|
+
* - Set any argument defaults from ConfigurationServiceProvider
|
|
488
|
+
* - Parse the arguments for the GlobalModifierCommand
|
|
489
|
+
* - Return on error
|
|
490
|
+
* - Add to list of GlobalModifierCommands to execute
|
|
491
|
+
* - Scan ConfigurationServiceProvider for GlobalModifierCommand clauses provided by the ServiceProvider not already found in arguments
|
|
492
|
+
* - For each configured clause:
|
|
493
|
+
* - Parse the arguments for the GlobalModifierCommand
|
|
494
|
+
* - Return on error
|
|
495
|
+
* - Add to list of GlobalModifierCommands to execute
|
|
496
|
+
* - Order the GlobalModifierCommand clauses in executePriority order
|
|
497
|
+
* - For each GlobalModifierCommand clause:
|
|
498
|
+
* - Execute the GlobalModifierCommand
|
|
499
|
+
* - Return on error
|
|
500
|
+
* - Init the ServiceProvider's service.
|
|
501
|
+
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by ServiceProviders
|
|
502
|
+
* - For each discovered clause:
|
|
503
|
+
* - Set any argument defaults from ConfigurationServiceProvider
|
|
504
|
+
* - Parse the arguments for the GlobalModifierCommand
|
|
505
|
+
* - Return on error
|
|
506
|
+
* - Add to list of GlobalModifierCommands to execute
|
|
507
|
+
* 3. Scan ConfigurationServiceProvider for GlobalModifierCommand clauses not provided by the ServiceProvider and not already found in arguments
|
|
508
|
+
* - For each discovered clause:
|
|
509
|
+
* - Parse the arguments for the GlobalModifierCommand
|
|
510
|
+
* - Return on error
|
|
511
|
+
* - Add to list of GlobalModifierCommands to execute
|
|
512
|
+
* 4. Order the GlobalModifierCommand clauses in executePriority order
|
|
513
|
+
* 5. For each GlobalModifierCommand clause:
|
|
514
|
+
* - Execute the GlobalModifierCommand
|
|
515
|
+
* - Return on error
|
|
516
|
+
* 6. Scan for a single non-modifier Command clause:
|
|
517
|
+
* - If found:
|
|
518
|
+
* - Set any argument defaults from ConfigurationServiceProvider
|
|
519
|
+
* - Parse the arguments for the non-modifier Command
|
|
520
|
+
* - Return on error
|
|
521
|
+
* - Warn on any unused arguments
|
|
522
|
+
* - Execute the non-modifier Command
|
|
523
|
+
* - Return on success or error
|
|
524
|
+
* 7. If default non-modifier Command provided:
|
|
525
|
+
* - Scan for default non-modifier Command clause
|
|
526
|
+
* - If found:
|
|
527
|
+
* - Set any argument defaults from ConfigurationServiceProvider
|
|
528
|
+
* - Parse the arguments for the non-modifier Command (but do not fail)
|
|
529
|
+
* - If parsing succeeds:
|
|
530
|
+
* - Warn on any unused arguments
|
|
531
|
+
* - Execute the non-modifier Command
|
|
532
|
+
* - Return on success or error
|
|
533
|
+
* - If not found:
|
|
534
|
+
* - Set any argument defaults from ConfigurationServiceProvider
|
|
535
|
+
* - Parse the arguments for the non-modifier Command (do not fail if required arguments are missing)
|
|
536
|
+
* - Execute the resulting non-modifier Command (fail on error)
|
|
537
|
+
*
|
|
538
|
+
* @param args the command line arguments (which do not include the invoked executable name).
|
|
539
|
+
* @param commandRegistry the {@link CommandRegistry} to use when scanning and parsing.
|
|
540
|
+
* @param serviceProviderRegistry the {@link ServiceProviderRegistry} to use when scanning and parsing.
|
|
541
|
+
* @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use for accessing argument defaults.
|
|
542
|
+
* @param context the {@link Context} in which to execute specified {@link Command} instances.
|
|
543
|
+
* @param defaultCommand optional {@link SubCommand} or {@link GlobalCommand} implementation to attempt to parse arguments for and execute if
|
|
544
|
+
* no non-modifier {@link Command} name is identified in the provided arguments.
|
|
545
|
+
*/
|
|
546
|
+
export async function run(
|
|
547
|
+
args: ReadonlyArray<string>,
|
|
548
|
+
commandRegistry: CommandRegistry,
|
|
549
|
+
serviceProviderRegistry: ServiceProviderRegistry,
|
|
550
|
+
configurationServiceProvider: ConfigurationServiceProvider | undefined,
|
|
551
|
+
context: Context,
|
|
552
|
+
defaultCommand?: Command,
|
|
553
|
+
): Promise<RunResult> {
|
|
554
|
+
if (
|
|
555
|
+
defaultCommand && !isSubCommand(defaultCommand) &&
|
|
556
|
+
!isGlobalCommand(defaultCommand)
|
|
557
|
+
) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
"If a default command is provided, if must be a global command or sub-command!",
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
// maintain a list of args still available for parsing
|
|
563
|
+
// the is an array because the provided args will be segmented into sequences by identified command clauses
|
|
564
|
+
// this is to prevent incorrectly parsing arguments as if they were interleaved across multiple commands which is not supported
|
|
565
|
+
let availableArgs: ReadonlyArray<ReadonlyArray<string>> = [args];
|
|
566
|
+
let unusedArgs: Array<ReadonlyArray<string>> = [];
|
|
567
|
+
|
|
568
|
+
// for each ServiceProvider (they are returned in servicePriority order)
|
|
569
|
+
for (const serviceProvider of serviceProviderRegistry.getServiceProviders()) {
|
|
570
|
+
// using the GlobalModifierCommands instances provided by the ServiceProvider...
|
|
571
|
+
const globalModifierCommandsByName = commandRegistry
|
|
572
|
+
.getGlobalModifierCommandsByNameProvidedByService(
|
|
573
|
+
serviceProvider.serviceId,
|
|
574
|
+
);
|
|
575
|
+
const globalModifierCommandsByShortAlias = commandRegistry
|
|
576
|
+
.getGlobalModifierCommandsByShortAliasProvidedByService(
|
|
577
|
+
serviceProvider.serviceId,
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// if there are GlobalModifierCommands provided by the ServiceProvider...
|
|
581
|
+
if (globalModifierCommandsByName.size > 0) {
|
|
582
|
+
// ...scan arguments, parse any resulting clauses and execute any resulting parsed GlobalModifierCommands
|
|
583
|
+
const runResult = await findAndExecuteGlobalModifierCommands(
|
|
584
|
+
availableArgs,
|
|
585
|
+
unusedArgs,
|
|
586
|
+
globalModifierCommandsByName,
|
|
587
|
+
globalModifierCommandsByShortAlias,
|
|
588
|
+
configurationServiceProvider,
|
|
589
|
+
context,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// nothing is returned unless a command was found and there was an error, in which case return the error
|
|
593
|
+
if (runResult) {
|
|
594
|
+
return runResult;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// update args for the next scan operation
|
|
598
|
+
availableArgs = unusedArgs;
|
|
599
|
+
unusedArgs = [];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// init the service provider's service
|
|
603
|
+
|
|
604
|
+
logger.debug("Initialising service with ID: %s", serviceProvider.serviceId);
|
|
605
|
+
|
|
606
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
607
|
+
configurationServiceProvider.setServiceKeyValueScope(
|
|
608
|
+
serviceProvider.serviceId,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
await serviceProvider.initService(context);
|
|
613
|
+
if (configurationServiceProvider?.keyValueServiceEnabled) {
|
|
614
|
+
await configurationServiceProvider.clearKeyValueScope();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// using the GlobalModifierCommands instances which are NOT provided by a ServiceProvider...
|
|
619
|
+
const globalModifierCommandsByName = commandRegistry
|
|
620
|
+
.getGlobalModifierCommandsByNameNotProvidedByService();
|
|
621
|
+
const globalModifierCommandsByShortAlias = commandRegistry
|
|
622
|
+
.getGlobalModifierCommandsByShortAliasNotProvidedByService();
|
|
623
|
+
|
|
624
|
+
// if there are GlobalModifierCommands NOT provided by the ServiceProvider...
|
|
625
|
+
if (globalModifierCommandsByName.size > 0) {
|
|
626
|
+
// ...scan arguments, parse any resulting clauses and execute any resulting parsed GlobalModifierCommands
|
|
627
|
+
const runResult = await findAndExecuteGlobalModifierCommands(
|
|
628
|
+
availableArgs,
|
|
629
|
+
unusedArgs,
|
|
630
|
+
globalModifierCommandsByName,
|
|
631
|
+
globalModifierCommandsByShortAlias,
|
|
632
|
+
configurationServiceProvider,
|
|
633
|
+
context,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// nothing is returned unless a command was found and there was an error, in which case return the error
|
|
637
|
+
if (runResult) {
|
|
638
|
+
return runResult;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// update args for the next scan operation
|
|
642
|
+
availableArgs = unusedArgs;
|
|
643
|
+
unusedArgs = [];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// using the non-modifier Command instances...
|
|
647
|
+
const nonModifierCommandsByName = commandRegistry
|
|
648
|
+
.getNonModifierCommandsByName();
|
|
649
|
+
const globalCommandsByShortAlias = commandRegistry
|
|
650
|
+
.getGlobalCommandsByShortAlias();
|
|
651
|
+
// and any group/member instance combinations...
|
|
652
|
+
const groupAndMemberCommandByJoinedName = commandRegistry
|
|
653
|
+
.getGroupAndMemberCommandsByJoinedName();
|
|
654
|
+
|
|
655
|
+
// ...scan arguments, parse a resulting clause and execute a resulting parsed non-modifier Command
|
|
656
|
+
let runResult = await findAndExecuteNonModifierCommand(
|
|
657
|
+
availableArgs,
|
|
658
|
+
unusedArgs,
|
|
659
|
+
nonModifierCommandsByName,
|
|
660
|
+
globalCommandsByShortAlias,
|
|
661
|
+
groupAndMemberCommandByJoinedName,
|
|
662
|
+
configurationServiceProvider,
|
|
663
|
+
context,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// nothing is returned if no command was discovered (otherwise a run result is returned)
|
|
667
|
+
if (runResult) {
|
|
668
|
+
return runResult;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// update args for the next scan operation
|
|
672
|
+
availableArgs = unusedArgs;
|
|
673
|
+
unusedArgs = [];
|
|
674
|
+
|
|
675
|
+
// if default non-modifier Command provided...
|
|
676
|
+
if (defaultCommand) {
|
|
677
|
+
// ...assume default command name and parse resulting clauses and execute a resulting parsed non-modifier Command if found
|
|
678
|
+
runResult = await findAndExecuteDefaultNonModifierCommand(
|
|
679
|
+
availableArgs,
|
|
680
|
+
unusedArgs,
|
|
681
|
+
defaultCommand,
|
|
682
|
+
configurationServiceProvider,
|
|
683
|
+
context,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// nothing is returned if no command was discovered (otherwise a run result is returned)
|
|
687
|
+
if (runResult) {
|
|
688
|
+
return runResult;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// give up if we haven't successfully parsed a non-modifier Command by now
|
|
693
|
+
await printNoCommandSpecifiedError(context);
|
|
694
|
+
return {
|
|
695
|
+
runState: RunState.NO_COMMAND,
|
|
696
|
+
};
|
|
697
|
+
}
|