@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,417 @@
1
+ import type GroupCommand from "../api/command/GroupCommand.ts";
2
+ import type GlobalModifierCommand from "../api/command/GlobalModifierCommand.ts";
3
+ import getLogger from "../util/logger.ts";
4
+ import type Command from "../api/command/Command.ts";
5
+ import type SubCommand from "../api/command/SubCommand.ts";
6
+ import { isGroupCommand } from "./command/CommandTypeGuards.ts";
7
+
8
+ const logger = getLogger("scanner");
9
+
10
+ /**
11
+ * A result from scanning arguments for commands.
12
+ */
13
+ export interface CommandClause {
14
+ /**
15
+ * The {@link Command} referenced as an argument.
16
+ */
17
+ command: Command;
18
+
19
+ /**
20
+ * Optional parent {@link GroupCommand} if {@link command} is a member {@link SubCommand}.
21
+ */
22
+ groupCommand?: GroupCommand;
23
+
24
+ /**
25
+ * The potential arguments for the discovered {@link Command}.
26
+ */
27
+ potentialArgs: Array<string>;
28
+ }
29
+
30
+ /**
31
+ * A container holding the result of a scan operation.
32
+ *
33
+ * At most one of {@link nonModifierCommandClause} or {@link globalModifierCommandClauses} will be populated.
34
+ * If none are populated then this means no command was found when scanning the provided arguments.
35
+ */
36
+ export interface ScanResult {
37
+ /**
38
+ * Optional clause containing a non-modifier {@link Command} found in the arguments.
39
+ */
40
+ nonModifierCommandClause?: CommandClause;
41
+
42
+ /**
43
+ * Optional clause containing a list of {@link GlobalModifierCommand} instances found in the arguments.
44
+ */
45
+ globalModifierCommandClauses?: Array<CommandClause>;
46
+
47
+ /**
48
+ * Any sequences of arguments which were unused in the scanning operation.
49
+ *
50
+ * As the scanning starts at the beginning of each provided argument sequence and a command clause is demarcated
51
+ * by a matching {@link Command} name (or {@link GlobalCommand} short alias), this will
52
+ * effectively be the leading argument sequences from those provided before the first {@link Command} was identified
53
+ * and any subsequent argument sequences which were not consumed by an identified {@link Command}.
54
+ */
55
+ unusedArgSequences: Array<ReadonlyArray<string>>;
56
+ }
57
+
58
+ function normaliseFirstArgumentIfRequired(
59
+ args: Array<string>,
60
+ globalCommandsByName: ReadonlyMap<string, Command>,
61
+ globalCommandsByShortAlias?: ReadonlyMap<string, Command>,
62
+ ): Array<string> {
63
+ const firstArg = args[0];
64
+
65
+ // only looking for:
66
+ // --<global_[modifier_]command_name>
67
+ // --<global_[modifier_]command_name>=<value>
68
+ // -<global_[modifier_]command_short_alias>
69
+ // -<global_[modifier_]command_short_alias>=<value>
70
+ if ((firstArg.length < 2) || (firstArg.charAt(0) !== "-")) {
71
+ return args;
72
+ }
73
+
74
+ // looking for:
75
+ // --<global_[modifier_]command_name>
76
+ // --<global_[modifier_]command_name>=<value>
77
+ if ((firstArg.length > 2) && (firstArg.charAt(1) === "-")) {
78
+ let potentialGlobalCommandName = firstArg.slice(2);
79
+
80
+ // looking for:
81
+ // <global_command_name>=<value>
82
+ // <global_modifier_command_name>=<value>
83
+ let nextArg: string | undefined;
84
+ if (potentialGlobalCommandName.includes("=")) {
85
+ [potentialGlobalCommandName, nextArg] = potentialGlobalCommandName.split(
86
+ /=(.*)/,
87
+ );
88
+ }
89
+
90
+ if (!globalCommandsByName.has(potentialGlobalCommandName)) {
91
+ // nothing changed
92
+ return args;
93
+ }
94
+ if (!nextArg) {
95
+ // nothing changed
96
+ return args;
97
+ }
98
+ logger.debug(() =>
99
+ `Normalised arg: '${firstArg}' to: '--${potentialGlobalCommandName} ${nextArg}'`
100
+ );
101
+ return [`--${potentialGlobalCommandName}`, nextArg, ...args.slice(1)];
102
+ }
103
+
104
+ // looking for:
105
+ // -<global_[modifier_]command_short_alias>
106
+ // -<global_[modifier_]command_short_alias>=<value>
107
+ // (by now we know the arg starts with -<char>)
108
+ let potentialGlobalCommandShortAlias = firstArg.slice(1);
109
+
110
+ // looking for:
111
+ // <global_[modifier_]command_short_alias>=<value>
112
+ let nextArg: string | undefined;
113
+ if (potentialGlobalCommandShortAlias.includes("=")) {
114
+ [potentialGlobalCommandShortAlias, nextArg] =
115
+ potentialGlobalCommandShortAlias.split(/=(.*)/);
116
+ }
117
+
118
+ if (!globalCommandsByShortAlias) {
119
+ // nothing changed
120
+ return args;
121
+ }
122
+
123
+ // looking for <global_[modifier_]command_short_alias>
124
+ const globalCommand = globalCommandsByShortAlias.get(
125
+ potentialGlobalCommandShortAlias,
126
+ );
127
+ if (!globalCommand) {
128
+ // nothing changed
129
+ return args;
130
+ }
131
+
132
+ if (nextArg) {
133
+ logger.debug(() =>
134
+ `Normalised arg: '${firstArg}' to: '--${globalCommand.name} ${nextArg}'`
135
+ );
136
+ return [`--${globalCommand.name}`, nextArg, ...args.slice(1)];
137
+ }
138
+ logger.debug(() =>
139
+ `Normalised arg: '${firstArg}' to: '--${globalCommand.name}'`
140
+ );
141
+ return [`--${globalCommand.name}`, ...args.slice(1)];
142
+ }
143
+
144
+ function scanForNextCommandClause(
145
+ potentialArgs: ReadonlyArray<string>,
146
+ includeSubCommands: boolean,
147
+ commandsByName: ReadonlyMap<string, Command>,
148
+ globalCommandsByShortAlias?: ReadonlyMap<string, Command>,
149
+ groupAndMemberCommandsByJoinedName?:
150
+ | ReadonlyMap<string, { groupCommand: GroupCommand; command: SubCommand }>
151
+ | undefined,
152
+ ): {
153
+ commandClause: CommandClause | undefined;
154
+ unusedArgs: Array<string>;
155
+ } {
156
+ let pendingArgs = [...potentialArgs];
157
+ const unusedArgs: Array<string> = [];
158
+
159
+ while (pendingArgs.length > 0) {
160
+ pendingArgs = normaliseFirstArgumentIfRequired(
161
+ pendingArgs,
162
+ commandsByName,
163
+ globalCommandsByShortAlias,
164
+ );
165
+
166
+ // take first argument
167
+ const currentArg = pendingArgs[0];
168
+ pendingArgs = pendingArgs.slice(1);
169
+
170
+ // looking for:
171
+ // <global_[modifier_]command_name>
172
+ if (currentArg.startsWith("--")) {
173
+ const potentialCommandName = currentArg.slice(2);
174
+
175
+ // check for Global[Modifier]Command name
176
+ const command = commandsByName.get(potentialCommandName);
177
+ if (command) {
178
+ logger.debug(
179
+ "Found global [modifier] command name: %s",
180
+ potentialCommandName,
181
+ );
182
+ return {
183
+ commandClause: {
184
+ command,
185
+ potentialArgs: pendingArgs,
186
+ },
187
+ unusedArgs,
188
+ };
189
+ }
190
+ }
191
+ if (!includeSubCommands || (currentArg[0] === "-")) {
192
+ unusedArgs.push(currentArg);
193
+ continue;
194
+ }
195
+
196
+ if (groupAndMemberCommandsByJoinedName) {
197
+ // looking for:
198
+ // <group_command_name>:<member_sub_command_name>
199
+ // <group_command_name> <member_sub_command_name>
200
+ // <sub_command_name>
201
+ let potentialGroupCommandName = currentArg;
202
+ let potentialMemberSubCommandName: string | undefined;
203
+ let potentialMemberCommandNameArgumentUsed = false;
204
+
205
+ if (potentialGroupCommandName.includes(":")) {
206
+ // looking for:
207
+ // <group_command_name>:<member_sub_command_name>
208
+ [potentialGroupCommandName, potentialMemberSubCommandName] =
209
+ potentialGroupCommandName.split(":");
210
+ } else if (pendingArgs.length > 0) {
211
+ // looking for:
212
+ // <group_command_name> <member_sub_command_name>
213
+ potentialMemberSubCommandName = pendingArgs[0];
214
+ pendingArgs = pendingArgs.slice(1);
215
+ potentialMemberCommandNameArgumentUsed = true;
216
+ }
217
+
218
+ if (potentialMemberSubCommandName !== undefined) {
219
+ const result = groupAndMemberCommandsByJoinedName.get(
220
+ `${potentialGroupCommandName}:${potentialMemberSubCommandName}`,
221
+ );
222
+ if (result && result.command) {
223
+ const { groupCommand, command } = result;
224
+ logger.debug(
225
+ "Found group command name: %s and member sub-command name: %s",
226
+ potentialGroupCommandName,
227
+ potentialMemberSubCommandName,
228
+ );
229
+ return {
230
+ commandClause: {
231
+ command,
232
+ groupCommand,
233
+ potentialArgs: pendingArgs,
234
+ },
235
+ unusedArgs,
236
+ };
237
+ }
238
+ }
239
+ // if group parsing was not needed, revert state if necessary
240
+ if (
241
+ potentialMemberCommandNameArgumentUsed &&
242
+ (potentialMemberSubCommandName !== undefined)
243
+ ) {
244
+ pendingArgs.unshift(potentialMemberSubCommandName!);
245
+ }
246
+ }
247
+
248
+ // looking for:
249
+ // <sub_command_name>
250
+ //
251
+ // if we find it is a group command we skip it as we didn't find a member command
252
+ // following it the logic above
253
+ const command = commandsByName.get(currentArg);
254
+ if (command && !isGroupCommand(command)) {
255
+ logger.debug("Found sub-command name: %s", currentArg);
256
+ return {
257
+ commandClause: {
258
+ command,
259
+ potentialArgs: pendingArgs,
260
+ },
261
+ unusedArgs,
262
+ };
263
+ }
264
+
265
+ // if we got here we haven't found a command so add the current arg to unused args
266
+ unusedArgs.push(currentArg);
267
+ }
268
+ // if we got here we haven't found a command and we have processed all provided args
269
+ return {
270
+ commandClause: undefined,
271
+ unusedArgs,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Scan the provided arguments and return any {@link CommandClause} instances discovered based on the provided
277
+ * list of {@link GlobalModifierCommand} instances.
278
+ *
279
+ * @param argSequences the list of argument sequences to scan.
280
+ * @param globalModifierCommandsByName the map of {@link GlobalModifierCommand} instances by name to use when scanning.
281
+ * @param globalModifierCommandsByShortAlias the map of {@link GlobalModifierCommand} instances by short alias to use when scanning.
282
+ */
283
+ export function scanForGlobalModifierCommandClauses(
284
+ argSequences: ReadonlyArray<ReadonlyArray<string>>,
285
+ globalModifierCommandsByName: ReadonlyMap<string, GlobalModifierCommand>,
286
+ globalModifierCommandsByShortAlias: ReadonlyMap<
287
+ string,
288
+ GlobalModifierCommand
289
+ >,
290
+ ): ScanResult {
291
+ logger.debug(() =>
292
+ `Scanning args: '${
293
+ JSON.stringify(argSequences)
294
+ }' for global modifier commands: '${
295
+ Array.from(globalModifierCommandsByName.keys()).join(" ")
296
+ }'`
297
+ );
298
+
299
+ const scanResult: ScanResult = {
300
+ unusedArgSequences: [],
301
+ };
302
+
303
+ const commandClauses: Array<CommandClause> = [];
304
+
305
+ for (const argSequence of argSequences) {
306
+ let lastCommandClause: CommandClause | undefined;
307
+ let args = argSequence;
308
+
309
+ while (args.length > 0) {
310
+ const { commandClause, unusedArgs } = scanForNextCommandClause(
311
+ args,
312
+ false,
313
+ globalModifierCommandsByName,
314
+ globalModifierCommandsByShortAlias,
315
+ undefined,
316
+ );
317
+
318
+ // any args that weren't used in the current scan are...
319
+ if (unusedArgs.length > 0) {
320
+ if (lastCommandClause) {
321
+ // ... given back to the previous scan result if defined
322
+ lastCommandClause.potentialArgs = unusedArgs;
323
+ } else {
324
+ // ... or added to the unused arg sequences
325
+ scanResult.unusedArgSequences.push(unusedArgs);
326
+ }
327
+ }
328
+
329
+ // if we didn't find a command clause all args have been allocated to
330
+ // a previous clause or the unuused arg sequences, so we can stop scanning
331
+ if (!commandClause) {
332
+ break;
333
+ }
334
+
335
+ // save the command we found
336
+ commandClauses.push(commandClause);
337
+
338
+ // now set the current scan result as the last scan result
339
+ lastCommandClause = commandClause;
340
+
341
+ // temporarily take all of the potential args from the last command clause and use them for a potential
342
+ // new command clause, if any args are left over they will be added back to the last command clause
343
+ args = lastCommandClause.potentialArgs;
344
+ lastCommandClause.potentialArgs = [];
345
+ }
346
+ }
347
+
348
+ if (commandClauses.length > 0) {
349
+ scanResult.globalModifierCommandClauses = commandClauses;
350
+ }
351
+
352
+ logger.debug("scan result: %j", scanResult);
353
+
354
+ return scanResult;
355
+ }
356
+
357
+ /**
358
+ * Scan the provided arguments and return the first {@link CommandClause} instance discovered based on the provided
359
+ * list of non-modifier {@link Command} instances.
360
+ *
361
+ * @param argSequences the list of argument sequences to scan.
362
+ * @param nonModifierCommandsByName the map of non-modifier {@link Command} instances by name to use when scanning.
363
+ * @param globalCommandsByShortAlias optional map of {@link GlobalCommand} instances by short alias to use when scanning.
364
+ * @param groupAndMemberCommandsByJoinedName optional map of {@link GroupCommand} and member {@link SubCommand} instances to use when scanning.
365
+ */
366
+ export function scanForNonModifierCommandClause(
367
+ argSequences: ReadonlyArray<ReadonlyArray<string>>,
368
+ nonModifierCommandsByName: ReadonlyMap<string, Command>,
369
+ globalCommandsByShortAlias?: ReadonlyMap<string, Command>,
370
+ groupAndMemberCommandsByJoinedName?: ReadonlyMap<
371
+ string,
372
+ { groupCommand: GroupCommand; command: SubCommand }
373
+ >,
374
+ ): ScanResult {
375
+ logger.debug(() =>
376
+ `Scanning args: '${
377
+ JSON.stringify(argSequences)
378
+ }' for non-modifier commands: '${
379
+ Array.from(nonModifierCommandsByName.keys()).join(" ")
380
+ }'`
381
+ );
382
+
383
+ const scanResult: ScanResult = {
384
+ unusedArgSequences: [],
385
+ };
386
+
387
+ for (const argSequence of argSequences) {
388
+ // if a command clause is already defined, we don't need to scan any further,
389
+ // just add the unused args to the unused arg sequences
390
+ if (scanResult.nonModifierCommandClause) {
391
+ scanResult.unusedArgSequences.push(argSequence);
392
+ continue;
393
+ }
394
+
395
+ const { commandClause, unusedArgs } = scanForNextCommandClause(
396
+ argSequence,
397
+ true,
398
+ nonModifierCommandsByName,
399
+ globalCommandsByShortAlias,
400
+ groupAndMemberCommandsByJoinedName,
401
+ );
402
+
403
+ // args that weren't used in the current scan are given to the unused arg sequences
404
+ if (unusedArgs.length > 0) {
405
+ scanResult.unusedArgSequences.push(unusedArgs);
406
+ }
407
+
408
+ // use the result if we found a command
409
+ if (commandClause) {
410
+ scanResult.nonModifierCommandClause = commandClause;
411
+ }
412
+ }
413
+
414
+ logger.debug("scan result: %j", scanResult);
415
+
416
+ return scanResult;
417
+ }
@@ -0,0 +1,41 @@
1
+ import type {
2
+ PopulatedArgumentSingleValueType,
3
+ PopulatedArgumentValues,
4
+ } from "../../api/argument/ArgumentValueTypes.ts";
5
+ import type { InvalidArgument } from "../../api/RunResult.ts";
6
+
7
+ /**
8
+ * A base interface for a container holding the result of argument value population.
9
+ */
10
+ export interface ValuePopulationResult {
11
+ /**
12
+ * Any args which were unused during argument value population.
13
+ */
14
+ readonly unusedArgs: ReadonlyArray<string>;
15
+
16
+ /**
17
+ * Any invalid argument discovered while populating values.
18
+ */
19
+ readonly invalidArgument?: InvalidArgument;
20
+ }
21
+
22
+ /**
23
+ * A container holding the result of argument values population for a {@link SubCommand}.
24
+ */
25
+ export interface SubCommandValuePopulationResult extends ValuePopulationResult {
26
+ /**
27
+ * Populated argument values.
28
+ */
29
+ readonly populatedArgumentValues: PopulatedArgumentValues;
30
+ }
31
+
32
+ /**
33
+ * A container holding the result of argument value population for a {@link GlobalCommand} or by extension a {@link GlobalModifierCommand}.
34
+ */
35
+ export interface GlobalCommandValuePopulationResult
36
+ extends ValuePopulationResult {
37
+ /**
38
+ * Populated argument value.
39
+ */
40
+ readonly populatedArgumentValue: PopulatedArgumentSingleValueType;
41
+ }
@@ -0,0 +1,201 @@
1
+ import type {
2
+ ArgumentSingleValueType,
3
+ ArgumentValues,
4
+ PopulatedArgumentSingleValueType,
5
+ PopulatedArgumentValues,
6
+ PopulatedArgumentValueType,
7
+ } from "../../api/argument/ArgumentValueTypes.ts";
8
+ import { MAXIMUM_ARGUMENT_ARRAY_SIZE } from "../../api/argument/SubCommandArgument.ts";
9
+ import { MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH } from "../../api/argument/ComplexOption.ts";
10
+
11
+ function doClone(
12
+ source:
13
+ | PopulatedArgumentValueType
14
+ | PopulatedArgumentValues
15
+ | Array<PopulatedArgumentValues | undefined>
16
+ | Array<PopulatedArgumentSingleValueType>,
17
+ nestingLevel: number,
18
+ ):
19
+ | PopulatedArgumentValueType
20
+ | PopulatedArgumentValues
21
+ | Array<PopulatedArgumentValues>
22
+ | Array<PopulatedArgumentSingleValueType> {
23
+ if (source === undefined) {
24
+ return source;
25
+ }
26
+ // check if source is an array
27
+ if (Array.isArray(source)) {
28
+ return arrayMerge(source, [], nestingLevel);
29
+ }
30
+ // check if source is an object
31
+ if (typeof source === "object") {
32
+ return objectMerge(source, {}, nestingLevel);
33
+ }
34
+ // source must be a primitive value
35
+ return source;
36
+ }
37
+
38
+ function arrayMerge(
39
+ override:
40
+ | Array<PopulatedArgumentSingleValueType>
41
+ | Array<PopulatedArgumentValues | undefined>,
42
+ base:
43
+ | Array<PopulatedArgumentSingleValueType>
44
+ | Array<PopulatedArgumentValues>,
45
+ nestingLevel: number,
46
+ ): Array<PopulatedArgumentSingleValueType> | Array<PopulatedArgumentValues> {
47
+ if (override.length > MAXIMUM_ARGUMENT_ARRAY_SIZE) {
48
+ throw new Error(`Maximum array size exceeded: ${override.length}`);
49
+ }
50
+ if (base.length > MAXIMUM_ARGUMENT_ARRAY_SIZE) {
51
+ throw new Error(`Maximum array size exceeded: ${override.length}`);
52
+ }
53
+ const result = base.slice();
54
+
55
+ override.forEach((argValue, index) => {
56
+ if (Array.isArray(argValue)) {
57
+ throw new Error(
58
+ `Array of array values discovered, this is not supported: ${
59
+ JSON.stringify(argValue)
60
+ }`,
61
+ );
62
+ }
63
+ if (index >= base.length) {
64
+ result.push(
65
+ doClone(argValue, nestingLevel) as
66
+ & PopulatedArgumentSingleValueType
67
+ & PopulatedArgumentValues,
68
+ );
69
+ } else {
70
+ result[index] = doMerge(argValue, base[index], nestingLevel) as
71
+ | PopulatedArgumentSingleValueType
72
+ | PopulatedArgumentValues;
73
+ }
74
+ });
75
+ return result;
76
+ }
77
+
78
+ function objectMerge(
79
+ override: PopulatedArgumentValues,
80
+ base: PopulatedArgumentValues,
81
+ nestingLevel: number,
82
+ ): PopulatedArgumentValues {
83
+ if (nestingLevel > MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH) {
84
+ throw new Error(
85
+ `Maximum complex option nesting depth exceeded: ${nestingLevel}`,
86
+ );
87
+ }
88
+
89
+ nestingLevel++;
90
+
91
+ const result: PopulatedArgumentValues = {};
92
+
93
+ Object.keys(override).forEach((argName) => {
94
+ if (base[argName] === undefined) {
95
+ // nothing to merge so just clone the override
96
+ result[argName] = doClone(override[argName], nestingLevel);
97
+ } else {
98
+ // merge the base and the override
99
+ result[argName] = doMerge(override[argName], base[argName], nestingLevel);
100
+ }
101
+ });
102
+ Object.keys(base).forEach((argName) => {
103
+ // if the base is in the override, we have already merged above
104
+ if (override[argName] !== undefined) {
105
+ return;
106
+ }
107
+ // nothing to merge so just clone the base
108
+ result[argName] = doClone(base[argName], nestingLevel);
109
+ });
110
+ return result;
111
+ }
112
+
113
+ function doMerge(
114
+ override:
115
+ | PopulatedArgumentValues
116
+ | PopulatedArgumentValueType
117
+ | Array<PopulatedArgumentValues | undefined>,
118
+ defaults:
119
+ | PopulatedArgumentValues
120
+ | PopulatedArgumentValueType
121
+ | Array<PopulatedArgumentValues | undefined>,
122
+ nestingLevel: number,
123
+ ):
124
+ | PopulatedArgumentValues
125
+ | PopulatedArgumentValueType
126
+ | Array<PopulatedArgumentValues> {
127
+ if (defaults === undefined) {
128
+ throw new Error(
129
+ `Undefined value provided in merge: override = ${
130
+ JSON.stringify(override)
131
+ }, defaults = undefined`,
132
+ );
133
+ }
134
+ if (typeof override !== typeof defaults) {
135
+ // check if we need to convert from single value to array
136
+ if (Array.isArray(override)) {
137
+ if (typeof defaults === "object") {
138
+ defaults = [defaults as ArgumentValues];
139
+ } else {
140
+ defaults = [defaults as ArgumentSingleValueType];
141
+ }
142
+ } else if (Array.isArray(defaults)) {
143
+ if (typeof override === "object") {
144
+ override = [override as PopulatedArgumentValues];
145
+ } else {
146
+ override = [override as PopulatedArgumentSingleValueType];
147
+ }
148
+ } // check if we need to convert from single non-string value to single string value
149
+ else if (
150
+ (typeof override === "string") &&
151
+ ((typeof defaults == "number") || (typeof defaults == "boolean"))
152
+ ) {
153
+ defaults = `${defaults}`;
154
+ }
155
+ }
156
+ if (override !== undefined && (typeof override !== typeof defaults)) {
157
+ throw new Error(
158
+ `Incompatible value types provided in merge: override = ${
159
+ JSON.stringify(override)
160
+ }, defaults = ${JSON.stringify(defaults)}`,
161
+ );
162
+ }
163
+
164
+ // check if override is an array
165
+ if (Array.isArray(override)) {
166
+ return arrayMerge(
167
+ override,
168
+ defaults as Array<ArgumentValues> | Array<ArgumentSingleValueType>,
169
+ nestingLevel,
170
+ );
171
+ }
172
+ // check if override is an object
173
+ if (typeof override === "object") {
174
+ return objectMerge(override, defaults as ArgumentValues, nestingLevel);
175
+ }
176
+
177
+ // check if override is undefined so we should use default
178
+ if (override === undefined) {
179
+ return doClone(defaults, nestingLevel);
180
+ }
181
+
182
+ // override must be a primitive value
183
+ return override;
184
+ }
185
+
186
+ /**
187
+ * Deep merge of provided default argument values with overridden argument values. The result is a cloned instance
188
+ * of `defaults` with values in `override` cloned and merged in the result.
189
+ *
190
+ * @throws {Error} if a value appears under the same argument name in both `override` and `defaults`.
191
+ * and they are not of the same type, or if either argument is `undefined`.
192
+ *
193
+ * @param override the values which should be used in place of any defined in `defaults`.
194
+ * @param defaults the values to use unless they are overridden in `override`.
195
+ */
196
+ export default function argumentValueMerge(
197
+ override: PopulatedArgumentValues,
198
+ defaults: PopulatedArgumentValues,
199
+ ): PopulatedArgumentValues {
200
+ return doMerge(override, defaults, 0) as PopulatedArgumentValues;
201
+ }