@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,549 @@
|
|
|
1
|
+
import type Command from "../../api/command/Command.ts";
|
|
2
|
+
import { isGroupCommand, isSubCommand } from "./CommandTypeGuards.ts";
|
|
3
|
+
import type GlobalCommand from "../../api/command/GlobalCommand.ts";
|
|
4
|
+
import type Argument from "../../api/argument/Argument.ts";
|
|
5
|
+
import type GroupCommand from "../../api/command/GroupCommand.ts";
|
|
6
|
+
import type SubCommand from "../../api/command/SubCommand.ts";
|
|
7
|
+
import type {
|
|
8
|
+
ArgumentSingleValueType,
|
|
9
|
+
} from "../../api/argument/ArgumentValueTypes.ts";
|
|
10
|
+
import {
|
|
11
|
+
ArgumentValueTypeName,
|
|
12
|
+
ComplexValueTypeName,
|
|
13
|
+
} from "../../api/argument/ArgumentValueTypes.ts";
|
|
14
|
+
import type Option from "../../api/argument/Option.ts";
|
|
15
|
+
import type ComplexOption from "../../api/argument/ComplexOption.ts";
|
|
16
|
+
import { isComplexOption } from "../argument/ArgumentTypeGuards.ts";
|
|
17
|
+
import {
|
|
18
|
+
getInvalidArgumentString,
|
|
19
|
+
validateGlobalCommandArgumentValue,
|
|
20
|
+
validateOptionValue,
|
|
21
|
+
} from "../values/argumentValueValidation.ts";
|
|
22
|
+
import getLogger from "../../util/logger.ts";
|
|
23
|
+
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
24
|
+
import {
|
|
25
|
+
getGlobalCommandArgumentConfigurationKey,
|
|
26
|
+
getSubCommandArgumentConfigurationKey,
|
|
27
|
+
} from "../../util/configHelper.ts";
|
|
28
|
+
import type { InvalidArgument } from "../../api/RunResult.ts";
|
|
29
|
+
import type SubCommandArgument from "../../api/argument/SubCommandArgument.ts";
|
|
30
|
+
import type GlobalCommandArgument from "../../api/argument/GlobalCommandArgument.ts";
|
|
31
|
+
|
|
32
|
+
const logger = getLogger("commandValidation");
|
|
33
|
+
|
|
34
|
+
function validateValueType(
|
|
35
|
+
type: ArgumentValueTypeName,
|
|
36
|
+
value: ArgumentSingleValueType,
|
|
37
|
+
argumentName: string,
|
|
38
|
+
isDefaultValue: boolean,
|
|
39
|
+
): void {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case ArgumentValueTypeName.BOOLEAN:
|
|
42
|
+
if (typeof value !== "boolean") {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Specified ${
|
|
45
|
+
isDefaultValue ? "default" : "allowable"
|
|
46
|
+
} value: '${value}' for argument: '${argumentName}' should be a boolean`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
case ArgumentValueTypeName.NUMBER:
|
|
51
|
+
if ((typeof value !== "number") || !Number.isFinite(value)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Specified ${
|
|
54
|
+
isDefaultValue ? "default" : "allowable"
|
|
55
|
+
} value: '${value}' for argument: '${argumentName}' should be a finite number`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case ArgumentValueTypeName.INTEGER:
|
|
60
|
+
if ((typeof value !== "number") || !Number.isInteger(value)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Specified ${
|
|
63
|
+
isDefaultValue ? "default" : "allowable"
|
|
64
|
+
} value: '${value}' for argument: '${argumentName}' should be an integer`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case ArgumentValueTypeName.STRING:
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Specified ${
|
|
72
|
+
isDefaultValue ? "default" : "allowable"
|
|
73
|
+
} value: '${value}' for argument: '${argumentName}' should be a string`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isAlphaNumeric(value: string): boolean {
|
|
81
|
+
return value.match(/^[a-z0-9]$/i) !== null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isAlphaNumericOrDashOrUnderscore(value: string): boolean {
|
|
85
|
+
return value.match(/^[a-z0-9\-_]+$/i) !== null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isUppercaseAlphaNumericOrUnderscore(value: string): boolean {
|
|
89
|
+
return value.match(/^[A-Z0-9_]+$/) !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNameLegal(name: string): boolean {
|
|
93
|
+
return (name.length > 0) && isAlphaNumericOrDashOrUnderscore(name) &&
|
|
94
|
+
(name[0] !== "-");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isConfigurationKeyLegal(configurationKey: string): boolean {
|
|
98
|
+
return (configurationKey.length > 0) &&
|
|
99
|
+
isUppercaseAlphaNumericOrUnderscore(configurationKey) &&
|
|
100
|
+
!configurationKey[0].match(/[0-9]/);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateArgument(argument: Argument, name: string): void {
|
|
104
|
+
if (
|
|
105
|
+
(argument.type !== ArgumentValueTypeName.NUMBER) &&
|
|
106
|
+
(argument.type !== ArgumentValueTypeName.INTEGER) &&
|
|
107
|
+
(argument.type !== ArgumentValueTypeName.STRING) &&
|
|
108
|
+
(argument.type !== ArgumentValueTypeName.BOOLEAN)
|
|
109
|
+
) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Illegal type: '${argument.type}' for argument: '${name}'`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (argument.allowableValues) {
|
|
115
|
+
argument.allowableValues.forEach((value) => {
|
|
116
|
+
validateValueType(argument.type, value, name, false);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateSubCommandArgument(
|
|
122
|
+
subCommandArgument: SubCommandArgument,
|
|
123
|
+
): void {
|
|
124
|
+
if (!isNameLegal(subCommandArgument.name)) {
|
|
125
|
+
throw new Error(`Illegal argument name: '${subCommandArgument.name}'`);
|
|
126
|
+
}
|
|
127
|
+
return validateArgument(subCommandArgument, subCommandArgument.name);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default class CommandValidator {
|
|
131
|
+
readonly #cliConfig: CLIConfig;
|
|
132
|
+
readonly #configurationKeys: Array<string> = [];
|
|
133
|
+
|
|
134
|
+
public constructor(cliConfig: CLIConfig) {
|
|
135
|
+
this.#cliConfig = cliConfig;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validates the provided {@link Command}.
|
|
140
|
+
*
|
|
141
|
+
* @param command the {@link Command} to validate.
|
|
142
|
+
*
|
|
143
|
+
* @throws {Error} if the provided command does not pass validation.
|
|
144
|
+
*/
|
|
145
|
+
public validate(command: Command): void {
|
|
146
|
+
logger.debug("Validating command: %s", command.name);
|
|
147
|
+
|
|
148
|
+
if (isSubCommand(command)) {
|
|
149
|
+
this.#validateSubCommand(command);
|
|
150
|
+
} else if (isGroupCommand(command)) {
|
|
151
|
+
this.#validateGroupCommand(command);
|
|
152
|
+
} else {
|
|
153
|
+
this.#validateGlobalCommand(command as GlobalCommand);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validates the provided {@link GlobalCommand} or by extension {@link GlobalModifierCommand}.
|
|
159
|
+
*
|
|
160
|
+
* @param globalCommand the {@link GlobalCommand} to validate.
|
|
161
|
+
*
|
|
162
|
+
* @throws {Error} if:
|
|
163
|
+
* * the {@link Command.name} does not consist only of alphanumeric non-whitespace ASCII characters or `_` and `-`
|
|
164
|
+
* characters.
|
|
165
|
+
* * the {@link Command.name} starts with `-`.
|
|
166
|
+
* * the {@link GlobalCommand.shortAlias} does not consist of a single alphanumeric non-whitespace ASCII character.
|
|
167
|
+
* * the {@link GlobalCommand.argument} is defined and it is not valid.
|
|
168
|
+
*/
|
|
169
|
+
#validateGlobalCommand(globalCommand: GlobalCommand): void {
|
|
170
|
+
if (!isNameLegal(globalCommand.name)) {
|
|
171
|
+
throw new Error(`Illegal global command name: '${globalCommand.name}'`);
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
globalCommand.shortAlias &&
|
|
175
|
+
(globalCommand.shortAlias.length !== 1 ||
|
|
176
|
+
!isAlphaNumeric(globalCommand.shortAlias))
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Illegal short alias: '${globalCommand.shortAlias}' for global command: '${globalCommand.name}'`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (globalCommand.argument) {
|
|
183
|
+
const { argument } = globalCommand;
|
|
184
|
+
validateArgument(argument, globalCommand.name);
|
|
185
|
+
if (argument.defaultValue) {
|
|
186
|
+
const invalidArguments: Array<InvalidArgument> = [];
|
|
187
|
+
validateGlobalCommandArgumentValue(
|
|
188
|
+
globalCommand,
|
|
189
|
+
argument.defaultValue,
|
|
190
|
+
invalidArguments,
|
|
191
|
+
);
|
|
192
|
+
if (invalidArguments.length > 0) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Illegal default value for global command: '${globalCommand.name}' => ${
|
|
195
|
+
invalidArguments.map((invalidArgument) =>
|
|
196
|
+
getInvalidArgumentString(invalidArgument, true)
|
|
197
|
+
).join(", ")
|
|
198
|
+
}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
this.#validateGlobalCommandArgumentConfigurationKey(
|
|
203
|
+
globalCommand,
|
|
204
|
+
argument,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validates the provided {@link GroupCommand}.
|
|
211
|
+
*
|
|
212
|
+
* @param groupCommand the {@link GroupCommand} to validate.
|
|
213
|
+
*
|
|
214
|
+
* @throws {Error} if:
|
|
215
|
+
* * the {@link Command.name} does not consist only of alphanumeric non-whitespace ASCII characters or `_` and `-`
|
|
216
|
+
* characters.
|
|
217
|
+
* * the {@link Command.name} starts with `-`.
|
|
218
|
+
* * {@link GroupCommand.memberSubCommands} is empty.
|
|
219
|
+
* * any of the member {@link SubCommand} instances are themselves not valid.
|
|
220
|
+
* * any of the member {@link SubCommand} instances define duplicate names.
|
|
221
|
+
* * any of the member {@link SubCommand} instances define a name which duplicates the group command name.
|
|
222
|
+
*/
|
|
223
|
+
#validateGroupCommand(groupCommand: GroupCommand): void {
|
|
224
|
+
if (!isNameLegal(groupCommand.name)) {
|
|
225
|
+
throw new Error(`Illegal group command name: '${groupCommand.name}'`);
|
|
226
|
+
}
|
|
227
|
+
if (groupCommand.memberSubCommands.length === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Group command: '${groupCommand.name}' has no member sub-commands`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const subCommandNames: Array<string> = [];
|
|
233
|
+
groupCommand.memberSubCommands.forEach((subCommand) => {
|
|
234
|
+
if (subCommandNames.includes(subCommand.name)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Group command: '${groupCommand.name}' contains member sub-commands with duplicate name: '${subCommand.name}'`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (subCommand.name === groupCommand.name) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Group command: '${groupCommand.name}' contains member sub-command with the same name`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
subCommandNames.push(subCommand.name);
|
|
245
|
+
this.#validateSubCommand(subCommand);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validates the provided {@link SubCommand}.
|
|
251
|
+
*
|
|
252
|
+
* @param subCommand the {@link SubCommand} to validate.
|
|
253
|
+
*
|
|
254
|
+
* @throws {Error} if:
|
|
255
|
+
* the {@link Command.name} does not consist only of alphanumeric non-whitespace ASCII characters or `_` and `-`
|
|
256
|
+
characters.
|
|
257
|
+
* the {@link Command.name} starts with `-`.
|
|
258
|
+
* * any [[SubCommandArgument.name]] name does not consist only of alphanumeric non-whitespace ASCII characters
|
|
259
|
+
* or `_` and `-` characters or starts with `-`.
|
|
260
|
+
* * any entry in {@link SubCommand.options} is not valid.
|
|
261
|
+
* * any entry in {@link SubCommand.positionals} is not valid.
|
|
262
|
+
* * there are duplicate names in the {@link SubCommandArgument} instances defined by {@link SubCommand.options} and
|
|
263
|
+
* {@link SubCommand.positionals}.
|
|
264
|
+
* * there are duplicate property paths defined by {@link Argument.name} and {@link Option.shortAlias} entries in nested
|
|
265
|
+
* {@link ComplexOption} instances.
|
|
266
|
+
*/
|
|
267
|
+
#validateSubCommand(subCommand: SubCommand): void {
|
|
268
|
+
if (!isNameLegal(subCommand.name)) {
|
|
269
|
+
throw new Error(`Illegal sub-command name: '${subCommand.name}'`);
|
|
270
|
+
}
|
|
271
|
+
const argumentNames: Array<string> = [];
|
|
272
|
+
const optionAliases: Array<string> = [];
|
|
273
|
+
const allOptionPaths: Array<string> = [];
|
|
274
|
+
if (subCommand.options) {
|
|
275
|
+
subCommand.options.forEach((option) => {
|
|
276
|
+
if (argumentNames.includes(option.name)) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Sub-command: '${subCommand.name}' contains arguments with the same name: '${option.name}'`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
argumentNames.push(option.name);
|
|
282
|
+
const currentOptionPaths: Array<string> = [""];
|
|
283
|
+
if (option.shortAlias) {
|
|
284
|
+
if (argumentNames.includes(option.shortAlias)) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Sub-command: '${subCommand.name}' contains arguments with the same name and short alias: '${option.shortAlias}'`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (optionAliases.includes(option.shortAlias)) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Sub-command: '${subCommand.name}' contains arguments with the same short alias: '${option.shortAlias}'`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
optionAliases.push(option.shortAlias);
|
|
295
|
+
}
|
|
296
|
+
this.#validateOptionOrComplexOption(
|
|
297
|
+
subCommand,
|
|
298
|
+
option,
|
|
299
|
+
currentOptionPaths,
|
|
300
|
+
allOptionPaths,
|
|
301
|
+
[],
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (subCommand.positionals) {
|
|
306
|
+
for (let i = 0; i < subCommand.positionals.length; i += 1) {
|
|
307
|
+
const positional = subCommand.positionals[i];
|
|
308
|
+
validateSubCommandArgument(positional);
|
|
309
|
+
if (argumentNames.includes(positional.name)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Sub-command: '${subCommand.name}' contains arguments with the same name: '${positional.name}'`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
argumentNames.push(positional.name);
|
|
315
|
+
|
|
316
|
+
if (
|
|
317
|
+
(i < subCommand.positionals.length - 1) &&
|
|
318
|
+
(positional.isVarargOptional || positional.isVarargMultiple)
|
|
319
|
+
) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Positional: '${positional.name}' for the command: '${subCommand.name}' is defined as a vararg but it is not the last positional argument`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
this.#validateSubCommandArgumentConfigurationKey(subCommand, [
|
|
325
|
+
positional,
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#validateSubCommandArgumentConfigurationKey(
|
|
332
|
+
command: Command,
|
|
333
|
+
argumentAncestry: Array<SubCommandArgument>,
|
|
334
|
+
) {
|
|
335
|
+
const argument = argumentAncestry[argumentAncestry.length - 1];
|
|
336
|
+
if (command.enableConfiguration === true) {
|
|
337
|
+
if (
|
|
338
|
+
(argument.configurationKey !== undefined) &&
|
|
339
|
+
!isConfigurationKeyLegal(argument.configurationKey)
|
|
340
|
+
) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Illegal configuration key: '${argument.configurationKey}'`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const configurationKey = getSubCommandArgumentConfigurationKey(
|
|
346
|
+
this.#cliConfig,
|
|
347
|
+
command,
|
|
348
|
+
argumentAncestry,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (configurationKey !== undefined) {
|
|
352
|
+
if (this.#configurationKeys.includes(configurationKey)) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Command: '${command.name}' contains arguments with the same configuration key: '${configurationKey}'`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
this.#configurationKeys.push(configurationKey);
|
|
358
|
+
}
|
|
359
|
+
} else if (argument.configurationKey !== undefined) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Command: '${command.name}' enableConfiguration is false, but an argument defines a configurationKey: '${argument.configurationKey}'`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#validateGlobalCommandArgumentConfigurationKey(
|
|
367
|
+
command: Command,
|
|
368
|
+
globalCommandArgument: GlobalCommandArgument,
|
|
369
|
+
) {
|
|
370
|
+
if (command.enableConfiguration === true) {
|
|
371
|
+
if (
|
|
372
|
+
(globalCommandArgument.configurationKey !== undefined) &&
|
|
373
|
+
!isConfigurationKeyLegal(globalCommandArgument.configurationKey)
|
|
374
|
+
) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Illegal configuration key: '${globalCommandArgument.configurationKey}'`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const configurationKey = getGlobalCommandArgumentConfigurationKey(
|
|
380
|
+
this.#cliConfig,
|
|
381
|
+
command,
|
|
382
|
+
globalCommandArgument,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
if (configurationKey !== undefined) {
|
|
386
|
+
if (this.#configurationKeys.includes(configurationKey)) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Command: '${command.name}' contains arguments with the same configuration key: '${configurationKey}'`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
this.#configurationKeys.push(configurationKey);
|
|
392
|
+
}
|
|
393
|
+
} else if (globalCommandArgument.configurationKey !== undefined) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`Command: '${command.name}' enableConfiguration is false, but an argument defines a configurationKey: '${globalCommandArgument.configurationKey}'`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#validateOptionOrComplexOption(
|
|
401
|
+
subCommand: SubCommand,
|
|
402
|
+
option: Option | ComplexOption,
|
|
403
|
+
currentOptionPaths: Array<string>,
|
|
404
|
+
allOptionPaths: Array<string>,
|
|
405
|
+
argumentAncestry: Array<SubCommandArgument>,
|
|
406
|
+
): void {
|
|
407
|
+
if (
|
|
408
|
+
option.shortAlias &&
|
|
409
|
+
(option.shortAlias.length !== 1 || !isAlphaNumeric(option.shortAlias))
|
|
410
|
+
) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`Illegal short alias: '${option.shortAlias}' for option: '${option.name}'`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (isComplexOption(option)) {
|
|
416
|
+
this.#validateComplexOption(
|
|
417
|
+
subCommand,
|
|
418
|
+
option,
|
|
419
|
+
currentOptionPaths,
|
|
420
|
+
allOptionPaths,
|
|
421
|
+
[...argumentAncestry, option as unknown as SubCommandArgument],
|
|
422
|
+
);
|
|
423
|
+
} else {
|
|
424
|
+
this.#validateOption(option, currentOptionPaths, allOptionPaths);
|
|
425
|
+
this.#validateSubCommandArgumentConfigurationKey(subCommand, [
|
|
426
|
+
...argumentAncestry,
|
|
427
|
+
option,
|
|
428
|
+
]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
#validateOption(
|
|
433
|
+
option: Option,
|
|
434
|
+
currentOptionPaths: Array<string>,
|
|
435
|
+
allOptionPaths: Array<string>,
|
|
436
|
+
): void {
|
|
437
|
+
validateSubCommandArgument(option);
|
|
438
|
+
if (Array.isArray(option.defaultValue) && !option.isArray) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`Illegal array default value: '${
|
|
441
|
+
option.defaultValue.join(",")
|
|
442
|
+
}' for non-array option: '${option.name}'`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (option.defaultValue) {
|
|
446
|
+
const invalidArguments: Array<InvalidArgument> = [];
|
|
447
|
+
validateOptionValue(
|
|
448
|
+
option,
|
|
449
|
+
option.defaultValue,
|
|
450
|
+
invalidArguments,
|
|
451
|
+
);
|
|
452
|
+
if (invalidArguments.length > 0) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Illegal default value for option: '${option.name}' => ${
|
|
455
|
+
invalidArguments.map((invalidArgument) =>
|
|
456
|
+
getInvalidArgumentString(invalidArgument, true)
|
|
457
|
+
).join(", ")
|
|
458
|
+
}`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
currentOptionPaths.forEach((currentOptionPath) => {
|
|
463
|
+
const leafPath = `${currentOptionPath}.${option.name}`;
|
|
464
|
+
if (allOptionPaths.includes(leafPath)) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Duplicate nested option path: '${leafPath}' discovered for option: '${option.name}'`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
allOptionPaths.push(leafPath);
|
|
470
|
+
});
|
|
471
|
+
if (option.shortAlias) {
|
|
472
|
+
currentOptionPaths.forEach((currentOptionPath) => {
|
|
473
|
+
const leafPath = `${currentOptionPath}.${option.shortAlias}`;
|
|
474
|
+
if (allOptionPaths.includes(leafPath)) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Duplicate nested option path: '${leafPath}' discovered for option: '${option.name}'`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
allOptionPaths.push(leafPath);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#validateComplexOption(
|
|
485
|
+
subCommand: SubCommand,
|
|
486
|
+
complexOption: ComplexOption,
|
|
487
|
+
currentOptionPaths: Array<string>,
|
|
488
|
+
allOptionPaths: Array<string>,
|
|
489
|
+
argumentAncestry: Array<SubCommandArgument>,
|
|
490
|
+
): void {
|
|
491
|
+
if (!isNameLegal(complexOption.name)) {
|
|
492
|
+
throw new Error(`Illegal complex option name: '${complexOption.name}'`);
|
|
493
|
+
}
|
|
494
|
+
if (complexOption.type !== ComplexValueTypeName.COMPLEX) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Illegal type: '${complexOption.type}' for complex option: '${complexOption.name}'`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(complexOption.defaultValue) && !complexOption.isArray) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Illegal array default value: '${
|
|
502
|
+
JSON.stringify(complexOption.defaultValue)
|
|
503
|
+
}' for non-array option: '${complexOption.name}'`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (complexOption.defaultValue) {
|
|
507
|
+
const invalidArguments: Array<InvalidArgument> = [];
|
|
508
|
+
validateOptionValue(
|
|
509
|
+
complexOption,
|
|
510
|
+
complexOption.defaultValue,
|
|
511
|
+
invalidArguments,
|
|
512
|
+
);
|
|
513
|
+
if (invalidArguments.length > 0) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Illegal default value for option: '${complexOption.name}' => ${
|
|
516
|
+
invalidArguments.map((invalidArgument) =>
|
|
517
|
+
getInvalidArgumentString(invalidArgument, true)
|
|
518
|
+
).join(", ")
|
|
519
|
+
}`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (complexOption.properties.length === 0) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Complex option: '${complexOption.name}' has no properties`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
complexOption.properties.forEach((property) => {
|
|
529
|
+
const newCurrentOptionPaths: Array<string> = [];
|
|
530
|
+
currentOptionPaths.forEach((currentOptionPath) => {
|
|
531
|
+
newCurrentOptionPaths.push(`${currentOptionPath}.${property.name}`);
|
|
532
|
+
});
|
|
533
|
+
if (property.shortAlias) {
|
|
534
|
+
currentOptionPaths.forEach((currentOptionPath) => {
|
|
535
|
+
newCurrentOptionPaths.push(
|
|
536
|
+
`${currentOptionPath}.${property.shortAlias}`,
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
this.#validateOptionOrComplexOption(
|
|
541
|
+
subCommand,
|
|
542
|
+
property,
|
|
543
|
+
newCurrentOptionPaths,
|
|
544
|
+
allOptionPaths,
|
|
545
|
+
[...argumentAncestry, complexOption as unknown as SubCommandArgument],
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|