@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,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
|
+
}
|