@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.
Files changed (133) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +1475 -0
  7. package/bun.lock +63 -0
  8. package/index.ts +89 -0
  9. package/package.json +23 -0
  10. package/src/api/CLI.ts +17 -0
  11. package/src/api/CLIConfig.ts +19 -0
  12. package/src/api/Context.ts +25 -0
  13. package/src/api/RunResult.ts +142 -0
  14. package/src/api/argument/Argument.ts +36 -0
  15. package/src/api/argument/ArgumentValueTypes.ts +87 -0
  16. package/src/api/argument/ComplexOption.ts +31 -0
  17. package/src/api/argument/GlobalCommandArgument.ts +17 -0
  18. package/src/api/argument/Option.ts +32 -0
  19. package/src/api/argument/Positional.ts +27 -0
  20. package/src/api/argument/SubCommandArgument.ts +20 -0
  21. package/src/api/command/Command.ts +22 -0
  22. package/src/api/command/GlobalCommand.ts +33 -0
  23. package/src/api/command/GlobalModifierCommand.ts +12 -0
  24. package/src/api/command/GroupCommand.ts +20 -0
  25. package/src/api/command/SubCommand.ts +41 -0
  26. package/src/api/command/UsageExample.ts +21 -0
  27. package/src/api/service/ServiceProvider.ts +52 -0
  28. package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
  29. package/src/api/service/core/KeyValueService.ts +28 -0
  30. package/src/api/service/core/PrinterService.ts +266 -0
  31. package/src/api/service/core/ShutdownService.ts +12 -0
  32. package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
  33. package/src/cli/BaseCLI.ts +312 -0
  34. package/src/cli/DefaultRuntimeCLI.ts +54 -0
  35. package/src/command/MultiCommandCliHelpCommand.ts +394 -0
  36. package/src/command/SingleCommandCliHelpCommand.ts +183 -0
  37. package/src/command/UsageCommand.ts +35 -0
  38. package/src/command/VersionCommand.ts +22 -0
  39. package/src/launcher.ts +127 -0
  40. package/src/runtime/DefaultContext.ts +32 -0
  41. package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
  42. package/src/runtime/command/CommandTypeGuards.ts +30 -0
  43. package/src/runtime/command/CommandValidator.ts +549 -0
  44. package/src/runtime/parser.ts +219 -0
  45. package/src/runtime/registry/CommandRegistry.ts +121 -0
  46. package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
  47. package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
  48. package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
  49. package/src/runtime/runner.ts +697 -0
  50. package/src/runtime/scanner.ts +417 -0
  51. package/src/runtime/values/ValuePopulationResult.ts +41 -0
  52. package/src/runtime/values/argumentValueMerge.ts +201 -0
  53. package/src/runtime/values/argumentValueValidation.ts +610 -0
  54. package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
  55. package/src/runtime/values/subCommandValuePopulation.ts +680 -0
  56. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
  57. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
  58. package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
  59. package/src/service/banner/BannerServiceProvider.ts +99 -0
  60. package/src/service/banner/command/NoBannerCommand.ts +42 -0
  61. package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
  62. package/src/service/configuration/DefaultKeyValueService.ts +60 -0
  63. package/src/service/configuration/command/ConfigCommand.ts +43 -0
  64. package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
  65. package/src/service/printer/DefaultPrinterService.ts +256 -0
  66. package/src/service/printer/PrinterServiceProvider.ts +61 -0
  67. package/src/service/printer/command/DarkModeCommand.ts +43 -0
  68. package/src/service/printer/command/LogLevelCommand.ts +68 -0
  69. package/src/service/printer/command/NoColorCommand.ts +43 -0
  70. package/src/service/printer/terminal/Ansi.ts +10 -0
  71. package/src/service/printer/terminal/Color.ts +14 -0
  72. package/src/service/printer/terminal/Progress.ts +373 -0
  73. package/src/service/printer/terminal/Spinner.ts +100 -0
  74. package/src/service/printer/terminal/Styler.ts +5 -0
  75. package/src/service/printer/terminal/Terminal.ts +8 -0
  76. package/src/service/printer/terminal/Theme.ts +34 -0
  77. package/src/service/printer/terminal/TtyStyler.ts +39 -0
  78. package/src/service/printer/terminal/TtyTerminal.ts +65 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +9 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
  81. package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
  83. package/src/util/configHelper.ts +95 -0
  84. package/src/util/envVarHelper.ts +338 -0
  85. package/src/util/helpHelper.ts +751 -0
  86. package/src/util/logger.ts +84 -0
  87. package/src/util/runnerHelper.ts +112 -0
  88. package/tests/cli/BaseCLI.test.ts +344 -0
  89. package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
  90. package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
  91. package/tests/command/UsageCommand.test.ts +53 -0
  92. package/tests/command/VersionCommand.test.ts +36 -0
  93. package/tests/fixtures/CLIConfig.ts +9 -0
  94. package/tests/fixtures/Command.ts +229 -0
  95. package/tests/fixtures/CommandRegistry.ts +8 -0
  96. package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
  97. package/tests/fixtures/Context.ts +33 -0
  98. package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
  99. package/tests/fixtures/StreamString.ts +37 -0
  100. package/tests/fixtures/util.ts +41 -0
  101. package/tests/runtime/DefaultContext.test.ts +20 -0
  102. package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
  103. package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
  104. package/tests/runtime/command/CommandValidator.test.ts +816 -0
  105. package/tests/runtime/parser.test.ts +766 -0
  106. package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
  107. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
  108. package/tests/runtime/runner.test.ts +1298 -0
  109. package/tests/runtime/scanner.test.ts +1113 -0
  110. package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
  111. package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
  112. package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
  113. package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
  114. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
  115. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
  116. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
  117. package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
  118. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
  119. package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
  120. package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
  121. package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
  122. package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
  123. package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
  124. package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
  125. package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
  126. package/tests/service/printer/terminal/Progress.test.ts +40 -0
  127. package/tests/service/printer/terminal/Spinner.test.ts +57 -0
  128. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
  129. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
  130. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
  131. package/tests/util/configHelper.test.ts +275 -0
  132. package/tests/util/envVarHelper.test.ts +318 -0
  133. 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
+ }