@flowscripter/dynamic-cli-framework 1.1.4 → 1.1.6
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/bun.lock +2 -2
- package/index.ts +1 -0
- package/package.json +2 -2
- package/src/api/argument/Argument.ts +16 -1
- package/src/api/argument/ArgumentValueTypes.ts +1 -1
- package/src/api/command/SubCommand.ts +1 -1
- package/src/api/service/core/PrinterService.ts +1 -1
- package/src/api/service/core/SyntaxHighlighterService.ts +1 -1
- package/src/command/MultiCommandCliHelpCommand.ts +2 -30
- package/src/runtime/command/CommandValidator.ts +67 -3
- package/src/runtime/runner.ts +10 -1
- package/src/runtime/values/argumentValueValidation.ts +47 -8
- package/src/service/printer/DefaultPrinterService.ts +3 -3
- package/src/service/printer/command/LogLevelCommand.ts +2 -1
- package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +19 -5
- package/src/util/helpHelper.ts +73 -1
- package/src/util/runnerHelper.ts +41 -0
- package/tests/runtime/command/CommandValidator.test.ts +103 -0
- package/tests/runtime/runner.test.ts +36 -0
- package/tests/runtime/values/argumentValueValidation.test.ts +22 -1
- package/tests/service/printer/DefaultPrinterService.test.ts +1 -1
- package/tests/service/printer/command/LogLevelCommand.test.ts +1 -1
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +1 -1
package/bun.lock
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"emphasize": "^7.0.0",
|
|
8
8
|
"fastest-levenshtein": "^1.0.16",
|
|
9
|
-
"figlet": "^1.8.
|
|
9
|
+
"figlet": "^1.8.1",
|
|
10
10
|
"highlight.js": "^11.11.1",
|
|
11
11
|
"prettier": "^3.5.3",
|
|
12
12
|
"supports-color": "^10.0.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],
|
|
45
45
|
|
|
46
|
-
"figlet": ["figlet@1.8.
|
|
46
|
+
"figlet": ["figlet@1.8.1", "", { "bin": { "figlet": "bin/index.js" } }, "sha512-kEC3Sme+YvA8Hkibv0NR1oClGcWia0VB2fC1SlMy027cwe795Xx40Xiv/nw/iFAwQLupymWh+uhAAErn/7hwPg=="],
|
|
47
47
|
|
|
48
48
|
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
|
49
49
|
|
package/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ export { default as ShutdownServiceProvider } from "./src/service/shutdown/Shutd
|
|
|
64
64
|
|
|
65
65
|
export { SYNTAX_HIGHLIGHTER_SERVICE_ID } from "./src/api/service/core/SyntaxHighlighterService.ts";
|
|
66
66
|
export type { default as SyntaxHighlighterService } from "./src/api/service/core/SyntaxHighlighterService.ts";
|
|
67
|
+
export type { ColorScheme } from "./src/api/service/core/SyntaxHighlighterService.ts";
|
|
67
68
|
export { default as DefaultSyntaxHighlighterService } from "./src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts";
|
|
68
69
|
export { default as SyntaxHighlighterServiceProvider } from "./src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts";
|
|
69
70
|
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"module": "index.ts",
|
|
20
20
|
"type": "module",
|
|
21
|
-
"version": "1.1.
|
|
21
|
+
"version": "1.1.6",
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"emphasize": "^7.0.0",
|
|
33
33
|
"fastest-levenshtein": "^1.0.16",
|
|
34
|
-
"figlet": "^1.8.
|
|
34
|
+
"figlet": "^1.8.1",
|
|
35
35
|
"highlight.js": "^11.11.1",
|
|
36
36
|
"prettier": "^3.5.3",
|
|
37
37
|
"supports-color": "^10.0.0",
|
|
@@ -13,10 +13,25 @@ export default interface Argument {
|
|
|
13
13
|
readonly type: ArgumentValueTypeName;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Optional list of values that the value must match.
|
|
16
|
+
* Optional list of values that the value must match. This is not supported for {@link ArgumentValueTypeName.BOOLEAN}.
|
|
17
17
|
*/
|
|
18
18
|
readonly allowableValues?: ReadonlyArray<ArgumentSingleValueType>;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Optional (default is `false`) for {@link ArgumentValueTypeName.STRING} when comparing a value against {@link allowableValues}.
|
|
22
|
+
*/
|
|
23
|
+
readonly isCaseInsensitive?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional for {@link ArgumentValueTypeName.INTEGER} or {@link ArgumentValueTypeName.NUMBER} when validating a value.
|
|
27
|
+
*/
|
|
28
|
+
readonly minValueInclusive?: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional for {@link ArgumentValueTypeName.INTEGER} or {@link ArgumentValueTypeName.NUMBER} when validating a value.
|
|
32
|
+
*/
|
|
33
|
+
readonly maxValueInclusive?: number;
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* Optional configuration key to use for the argument. Must consist of alphanumeric non-whitespace uppercase
|
|
22
37
|
* ASCII or `_` characters. Must not start with a digit.
|
|
@@ -21,7 +21,7 @@ export default interface SubCommand extends Command {
|
|
|
21
21
|
readonly positionals: ReadonlyArray<Positional>;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Optional grouping topic of the command
|
|
24
|
+
* Optional grouping topic of the command for structuring of help output.
|
|
25
25
|
*/
|
|
26
26
|
readonly helpTopic?: string;
|
|
27
27
|
|
|
@@ -139,7 +139,7 @@ export default interface PrinterService {
|
|
|
139
139
|
* Has no effect if {@link colorEnabled} is `false`.
|
|
140
140
|
*
|
|
141
141
|
* @param message the message to color.
|
|
142
|
-
* @param hexFormattedColor the color to use. This should be a valid hex formatted string e.g. "
|
|
142
|
+
* @param hexFormattedColor the color to use. This should be a valid hex formatted string e.g. "#rrggbb".
|
|
143
143
|
*/
|
|
144
144
|
color(message: string, hexFormattedColor: string): string;
|
|
145
145
|
|
|
@@ -8,7 +8,7 @@ export const SYNTAX_HIGHLIGHTER_SERVICE_ID =
|
|
|
8
8
|
*
|
|
9
9
|
* The keys are the names of the defined [highlight.js scopes](https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html).
|
|
10
10
|
*
|
|
11
|
-
* The values are colors expressed as a hex formatted string e.g. "
|
|
11
|
+
* The values are colors expressed as a hex formatted string e.g. "#rrggbb".
|
|
12
12
|
*
|
|
13
13
|
* The scheme does not need to list all scopes exhaustively.
|
|
14
14
|
*/
|
|
@@ -3,7 +3,6 @@ import type Option from "../api/argument/Option.ts";
|
|
|
3
3
|
import type SubCommand from "../api/command/SubCommand.ts";
|
|
4
4
|
import type GlobalCommand from "../api/command/GlobalCommand.ts";
|
|
5
5
|
import type GroupCommand from "../api/command/GroupCommand.ts";
|
|
6
|
-
import { distance } from "fastest-levenshtein";
|
|
7
6
|
import {
|
|
8
7
|
type ArgumentSingleValueType,
|
|
9
8
|
type ArgumentValues,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
import type Context from "../api/Context.ts";
|
|
13
12
|
import type { HelpSection } from "../util/helpHelper.ts";
|
|
14
13
|
import {
|
|
14
|
+
findPossibleCommandNames,
|
|
15
15
|
getCommandArgsHelpSections,
|
|
16
16
|
getCommandExamplesHelpSections,
|
|
17
17
|
getGlobalArgumentHelpEntry,
|
|
@@ -44,34 +44,6 @@ abstract class MultiCommandCliAbstractHelpCommand {
|
|
|
44
44
|
this.#commandRegistry = commandRegistry;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
#findPossibleCommandNames(
|
|
48
|
-
commandName: string,
|
|
49
|
-
groupCommands: ReadonlyArray<GroupCommand>,
|
|
50
|
-
subCommands: ReadonlyArray<SubCommand>,
|
|
51
|
-
): string[] {
|
|
52
|
-
const levenCommandArray = new Array<[number, string]>();
|
|
53
|
-
subCommands.forEach((subCommand) => {
|
|
54
|
-
levenCommandArray.push([
|
|
55
|
-
distance(subCommand.name, commandName),
|
|
56
|
-
subCommand.name,
|
|
57
|
-
]);
|
|
58
|
-
});
|
|
59
|
-
groupCommands.forEach((groupCommand) => {
|
|
60
|
-
groupCommand.memberSubCommands.forEach((memberCommand) => {
|
|
61
|
-
const memberName = `${groupCommand.name}:${memberCommand.name}`;
|
|
62
|
-
levenCommandArray.push([
|
|
63
|
-
distance(memberName, commandName),
|
|
64
|
-
memberName,
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
return levenCommandArray
|
|
69
|
-
.sort((a, b) => a[0] - b[0])
|
|
70
|
-
.slice(0, 2)
|
|
71
|
-
.filter((value) => value[0] < 3)
|
|
72
|
-
.map((value) => value[1]);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
47
|
#getGlobalCommandsHelpSection(
|
|
76
48
|
context: Context,
|
|
77
49
|
title: string,
|
|
@@ -273,7 +245,7 @@ abstract class MultiCommandCliAbstractHelpCommand {
|
|
|
273
245
|
);
|
|
274
246
|
|
|
275
247
|
// look for other possible matches
|
|
276
|
-
const possibleCommandNames =
|
|
248
|
+
const possibleCommandNames = findPossibleCommandNames(
|
|
277
249
|
commandName,
|
|
278
250
|
groupCommands,
|
|
279
251
|
subCommands,
|
|
@@ -36,6 +36,7 @@ function validateValueType(
|
|
|
36
36
|
value: ArgumentSingleValueType,
|
|
37
37
|
argumentName: string,
|
|
38
38
|
isDefaultValue: boolean,
|
|
39
|
+
isRangeValue: boolean,
|
|
39
40
|
): void {
|
|
40
41
|
switch (type) {
|
|
41
42
|
case ArgumentValueTypeName.BOOLEAN:
|
|
@@ -51,7 +52,7 @@ function validateValueType(
|
|
|
51
52
|
if ((typeof value !== "number") || !Number.isFinite(value)) {
|
|
52
53
|
throw new Error(
|
|
53
54
|
`Specified ${
|
|
54
|
-
isDefaultValue ? "default" : "allowable"
|
|
55
|
+
isDefaultValue ? "default" : isRangeValue ? "range" : "allowable"
|
|
55
56
|
} value: '${value}' for argument: '${argumentName}' should be a finite number`,
|
|
56
57
|
);
|
|
57
58
|
}
|
|
@@ -60,7 +61,7 @@ function validateValueType(
|
|
|
60
61
|
if ((typeof value !== "number") || !Number.isInteger(value)) {
|
|
61
62
|
throw new Error(
|
|
62
63
|
`Specified ${
|
|
63
|
-
isDefaultValue ? "default" : "allowable"
|
|
64
|
+
isDefaultValue ? "default" : isRangeValue ? "range" : "allowable"
|
|
64
65
|
} value: '${value}' for argument: '${argumentName}' should be an integer`,
|
|
65
66
|
);
|
|
66
67
|
}
|
|
@@ -112,10 +113,73 @@ function validateArgument(argument: Argument, name: string): void {
|
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
115
|
if (argument.allowableValues) {
|
|
116
|
+
if (argument.type === ArgumentValueTypeName.BOOLEAN) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Illegal type: '${argument.type}' for argument: '${name}' with allowable values specified`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
115
121
|
argument.allowableValues.forEach((value) => {
|
|
116
|
-
validateValueType(argument.type, value, name, false);
|
|
122
|
+
validateValueType(argument.type, value, name, false, false);
|
|
117
123
|
});
|
|
118
124
|
}
|
|
125
|
+
if (argument.isCaseInsensitive !== undefined) {
|
|
126
|
+
if (
|
|
127
|
+
(argument.type === ArgumentValueTypeName.BOOLEAN) &&
|
|
128
|
+
(argument.isCaseInsensitive === false)
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Illegal type: '${argument.type}' for argument: '${name}' must always be case insensitive`,
|
|
132
|
+
);
|
|
133
|
+
} else if (argument.type !== ArgumentValueTypeName.STRING) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Illegal type: '${argument.type}' for argument: '${name}' with case insensitive specified`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (argument.minValueInclusive !== undefined) {
|
|
140
|
+
if (
|
|
141
|
+
(argument.type !== ArgumentValueTypeName.NUMBER) &&
|
|
142
|
+
(argument.type !== ArgumentValueTypeName.INTEGER)
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Illegal type: '${argument.type}' for argument: '${name}' with min value specified`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (argument.allowableValues) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Illegal combination of allowable values and min value for argument: '${name}'`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
validateValueType(
|
|
154
|
+
argument.type,
|
|
155
|
+
argument.minValueInclusive,
|
|
156
|
+
name,
|
|
157
|
+
false,
|
|
158
|
+
true,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (argument.maxValueInclusive !== undefined) {
|
|
162
|
+
if (
|
|
163
|
+
(argument.type !== ArgumentValueTypeName.NUMBER) &&
|
|
164
|
+
(argument.type !== ArgumentValueTypeName.INTEGER)
|
|
165
|
+
) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Illegal type: '${argument.type}' for argument: '${name}' with max value specified`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (argument.allowableValues) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Illegal combination of allowable values and max value for argument: '${name}'`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
validateValueType(
|
|
176
|
+
argument.type,
|
|
177
|
+
argument.maxValueInclusive,
|
|
178
|
+
name,
|
|
179
|
+
false,
|
|
180
|
+
true,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
function validateSubCommandArgument(
|
package/src/runtime/runner.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import getLogger from "../util/logger.ts";
|
|
13
13
|
import {
|
|
14
14
|
printCommandExecutionError,
|
|
15
|
+
printNoCommandRecognisedError,
|
|
15
16
|
printNoCommandSpecifiedError,
|
|
16
17
|
printParseResultError,
|
|
17
18
|
printUnusedArgsWarning,
|
|
@@ -690,7 +691,15 @@ export async function run(
|
|
|
690
691
|
}
|
|
691
692
|
|
|
692
693
|
// give up if we haven't successfully parsed a non-modifier Command by now
|
|
693
|
-
|
|
694
|
+
if (availableArgs.length > 0) {
|
|
695
|
+
await printNoCommandRecognisedError(
|
|
696
|
+
context,
|
|
697
|
+
commandRegistry,
|
|
698
|
+
availableArgs.flat(),
|
|
699
|
+
);
|
|
700
|
+
} else {
|
|
701
|
+
await printNoCommandSpecifiedError(context);
|
|
702
|
+
}
|
|
694
703
|
return {
|
|
695
704
|
runState: RunState.NO_COMMAND,
|
|
696
705
|
};
|
|
@@ -111,15 +111,54 @@ function validatePrimitiveValue(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// check if the value is valid
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
|
|
115
|
+
if (argument.allowableValues) {
|
|
116
|
+
let searchValue = convertedValue;
|
|
117
|
+
let allowableValues = argument.allowableValues;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
(argument.type === ArgumentValueTypeName.STRING) &&
|
|
121
|
+
argument.isCaseInsensitive
|
|
122
|
+
) {
|
|
123
|
+
searchValue = convertedValue.toLowerCase();
|
|
124
|
+
allowableValues = argument.allowableValues.map((v) =>
|
|
125
|
+
(v as string).toLowerCase()
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!allowableValues.includes(searchValue)) {
|
|
129
|
+
return {
|
|
130
|
+
invalidArgument: {
|
|
131
|
+
argument,
|
|
132
|
+
value,
|
|
133
|
+
reason: InvalidArgumentReason.ILLEGAL_VALUE,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (argument.minValueInclusive !== undefined) {
|
|
139
|
+
if (convertedValue < argument.minValueInclusive) {
|
|
140
|
+
return {
|
|
141
|
+
invalidArgument: {
|
|
142
|
+
argument,
|
|
143
|
+
value,
|
|
144
|
+
reason: InvalidArgumentReason.ILLEGAL_VALUE,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
122
148
|
}
|
|
149
|
+
|
|
150
|
+
if (argument.maxValueInclusive !== undefined) {
|
|
151
|
+
if (convertedValue > argument.maxValueInclusive) {
|
|
152
|
+
return {
|
|
153
|
+
invalidArgument: {
|
|
154
|
+
argument,
|
|
155
|
+
value,
|
|
156
|
+
reason: InvalidArgumentReason.ILLEGAL_VALUE,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
123
162
|
return { validValue: convertedValue };
|
|
124
163
|
}
|
|
125
164
|
|
|
@@ -163,13 +163,13 @@ export default class DefaultPrinterService implements PrinterService {
|
|
|
163
163
|
|
|
164
164
|
public color(message: string, hexFormattedColor: string): string {
|
|
165
165
|
if (
|
|
166
|
-
(hexFormattedColor.length !==
|
|
167
|
-
(!hexFormattedColor.toLowerCase().startsWith("
|
|
166
|
+
(hexFormattedColor.length !== 7) ||
|
|
167
|
+
(!hexFormattedColor.toLowerCase().startsWith("#"))
|
|
168
168
|
) {
|
|
169
169
|
throw new Error(`Invalid color: ${hexFormattedColor}`);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const colorValue = parseInt(hexFormattedColor, 16);
|
|
172
|
+
const colorValue = parseInt(hexFormattedColor.slice(1), 16);
|
|
173
173
|
|
|
174
174
|
if (isNaN(colorValue) || (colorValue < 0 || colorValue > 0xffffff)) {
|
|
175
175
|
throw new Error(`Invalid color: ${hexFormattedColor}`);
|
|
@@ -23,6 +23,7 @@ export default class LogLevelCommand implements GlobalModifierCommand {
|
|
|
23
23
|
"WARN",
|
|
24
24
|
"ERROR",
|
|
25
25
|
],
|
|
26
|
+
isCaseInsensitive: true,
|
|
26
27
|
defaultValue: "INFO",
|
|
27
28
|
configurationKey: "LOG_LEVEL",
|
|
28
29
|
};
|
|
@@ -44,7 +45,7 @@ export default class LogLevelCommand implements GlobalModifierCommand {
|
|
|
44
45
|
): Promise<void> {
|
|
45
46
|
const logLevel = argumentValue as string;
|
|
46
47
|
|
|
47
|
-
switch (logLevel) {
|
|
48
|
+
switch (logLevel.toUpperCase()) {
|
|
48
49
|
case "DEBUG":
|
|
49
50
|
this.#printerServiceProvider.printerService!.setLevel(
|
|
50
51
|
Level.DEBUG,
|
|
@@ -2,9 +2,9 @@ import type SyntaxHighlighterService from "../../api/service/core/SyntaxHighligh
|
|
|
2
2
|
import type { LanguageFn as HighlightSyntax } from "highlight.js";
|
|
3
3
|
import { createEmphasize } from "emphasize";
|
|
4
4
|
import json from "highlight.js/lib/languages/json";
|
|
5
|
-
import { ColorScheme } from "../../api/service/core/SyntaxHighlighterService.ts";
|
|
5
|
+
import type { ColorScheme } from "../../api/service/core/SyntaxHighlighterService.ts";
|
|
6
6
|
|
|
7
|
-
type Sheet = Record<string, (string) => string> | undefined;
|
|
7
|
+
type Sheet = Record<string, (value: string) => string> | undefined;
|
|
8
8
|
|
|
9
9
|
interface Emphasize {
|
|
10
10
|
listLanguages(): ReadonlyArray<string>;
|
|
@@ -24,7 +24,8 @@ interface Emphasize {
|
|
|
24
24
|
export default class DefaultSyntaxHighlighterService
|
|
25
25
|
implements SyntaxHighlighterService {
|
|
26
26
|
colorEnabled = true;
|
|
27
|
-
colorFunction: (text: string, hexFormattedColor: string) => string
|
|
27
|
+
colorFunction: (text: string, hexFormattedColor: string) => string = (text) =>
|
|
28
|
+
text;
|
|
28
29
|
|
|
29
30
|
emphasize: Emphasize;
|
|
30
31
|
|
|
@@ -58,9 +59,22 @@ export default class DefaultSyntaxHighlighterService
|
|
|
58
59
|
if (colorScheme && this.colorFunction) {
|
|
59
60
|
sheet = {};
|
|
60
61
|
for (const [key, value] of Object.entries(colorScheme)) {
|
|
61
|
-
if (value) {
|
|
62
|
-
|
|
62
|
+
if (!value) {
|
|
63
|
+
throw new Error(`Invalid color for entry: ${key}`);
|
|
63
64
|
}
|
|
65
|
+
if (
|
|
66
|
+
(value.length !== 7) ||
|
|
67
|
+
(!value.toLowerCase().startsWith("#"))
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(`Invalid color: ${value}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const colorValue = parseInt(value.slice(1), 16);
|
|
73
|
+
|
|
74
|
+
if (isNaN(colorValue) || (colorValue < 0 || colorValue > 0xffffff)) {
|
|
75
|
+
throw new Error(`Invalid color: ${value}`);
|
|
76
|
+
}
|
|
77
|
+
sheet[key] = (text: string) => this.colorFunction(text, value);
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
80
|
|
package/src/util/helpHelper.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { distance } from "fastest-levenshtein";
|
|
1
2
|
import type GlobalCommand from "../api/command/GlobalCommand.ts";
|
|
2
3
|
import type GlobalCommandArgument from "../api/argument/GlobalCommandArgument.ts";
|
|
3
4
|
import {
|
|
@@ -267,6 +268,20 @@ export function getGlobalArgumentHelpEntry(
|
|
|
267
268
|
notesItems.push("number value");
|
|
268
269
|
break;
|
|
269
270
|
}
|
|
271
|
+
if (
|
|
272
|
+
(argument.minValueInclusive !== undefined) ||
|
|
273
|
+
(argument.maxValueInclusive !== undefined)
|
|
274
|
+
) {
|
|
275
|
+
let range = "..";
|
|
276
|
+
|
|
277
|
+
if (argument.minValueInclusive !== undefined) {
|
|
278
|
+
range = argument.minValueInclusive + range;
|
|
279
|
+
}
|
|
280
|
+
if (argument.maxValueInclusive !== undefined) {
|
|
281
|
+
range += argument.maxValueInclusive;
|
|
282
|
+
}
|
|
283
|
+
notesItems.push(range);
|
|
284
|
+
}
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
if (argument.defaultValue !== undefined) {
|
|
@@ -399,6 +414,21 @@ function getOptionHelpEntry(
|
|
|
399
414
|
}
|
|
400
415
|
}
|
|
401
416
|
}
|
|
417
|
+
if (
|
|
418
|
+
(option.minValueInclusive !== undefined) ||
|
|
419
|
+
(option.maxValueInclusive !== undefined)
|
|
420
|
+
) {
|
|
421
|
+
let range = "..";
|
|
422
|
+
|
|
423
|
+
if (option.minValueInclusive !== undefined) {
|
|
424
|
+
range = option.minValueInclusive + range;
|
|
425
|
+
}
|
|
426
|
+
if (option.maxValueInclusive !== undefined) {
|
|
427
|
+
range += option.maxValueInclusive;
|
|
428
|
+
}
|
|
429
|
+
notesItems.push(range);
|
|
430
|
+
}
|
|
431
|
+
|
|
402
432
|
if (option.defaultValue !== undefined) {
|
|
403
433
|
notesItems.push(
|
|
404
434
|
`default: ${
|
|
@@ -480,6 +510,20 @@ function getPositionalHelpEntry(
|
|
|
480
510
|
break;
|
|
481
511
|
}
|
|
482
512
|
}
|
|
513
|
+
if (
|
|
514
|
+
(positional.minValueInclusive !== undefined) ||
|
|
515
|
+
(positional.maxValueInclusive !== undefined)
|
|
516
|
+
) {
|
|
517
|
+
let range = "..";
|
|
518
|
+
|
|
519
|
+
if (positional.minValueInclusive !== undefined) {
|
|
520
|
+
range = positional.minValueInclusive + range;
|
|
521
|
+
}
|
|
522
|
+
if (positional.maxValueInclusive !== undefined) {
|
|
523
|
+
range += positional.maxValueInclusive;
|
|
524
|
+
}
|
|
525
|
+
notesItems.push(range);
|
|
526
|
+
}
|
|
483
527
|
|
|
484
528
|
if (positional.isVarargOptional) {
|
|
485
529
|
notesItems.push("optional");
|
|
@@ -633,7 +677,7 @@ export function getMultiCommandAppSyntax(
|
|
|
633
677
|
let argOptional = false;
|
|
634
678
|
let argValueOptional = false;
|
|
635
679
|
let multipleArg = false;
|
|
636
|
-
const commandClauses = [];
|
|
680
|
+
const commandClauses: string[] = [];
|
|
637
681
|
if (globalCommands.length > 0) {
|
|
638
682
|
commandClauses.push(`<${globalPrefix}command>`);
|
|
639
683
|
arg = globalCommands.some((
|
|
@@ -749,3 +793,31 @@ export function getMultiCommandAppSyntax(
|
|
|
749
793
|
}
|
|
750
794
|
return syntax;
|
|
751
795
|
}
|
|
796
|
+
|
|
797
|
+
export function findPossibleCommandNames(
|
|
798
|
+
commandName: string,
|
|
799
|
+
groupCommands: ReadonlyArray<GroupCommand>,
|
|
800
|
+
subCommands: ReadonlyArray<SubCommand>,
|
|
801
|
+
): string[] {
|
|
802
|
+
const levenCommandArray = new Array<[number, string]>();
|
|
803
|
+
subCommands.forEach((subCommand) => {
|
|
804
|
+
levenCommandArray.push([
|
|
805
|
+
distance(subCommand.name, commandName),
|
|
806
|
+
subCommand.name,
|
|
807
|
+
]);
|
|
808
|
+
});
|
|
809
|
+
groupCommands.forEach((groupCommand) => {
|
|
810
|
+
groupCommand.memberSubCommands.forEach((memberCommand) => {
|
|
811
|
+
const memberName = `${groupCommand.name}:${memberCommand.name}`;
|
|
812
|
+
levenCommandArray.push([
|
|
813
|
+
distance(memberName, commandName),
|
|
814
|
+
memberName,
|
|
815
|
+
]);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
return levenCommandArray
|
|
819
|
+
.sort((a, b) => a[0] - b[0])
|
|
820
|
+
.slice(0, 2)
|
|
821
|
+
.filter((value) => value[0] < 3)
|
|
822
|
+
.map((value) => value[1]);
|
|
823
|
+
}
|
package/src/util/runnerHelper.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../runtime/command/CommandTypeGuards.ts";
|
|
11
11
|
import type Context from "../api/Context.ts";
|
|
12
12
|
import { getInvalidArgumentString } from "../runtime/values/argumentValueValidation.ts";
|
|
13
|
+
import CommandRegistry from "../runtime/registry/CommandRegistry.ts";
|
|
14
|
+
import { findPossibleCommandNames } from "./helpHelper.ts";
|
|
13
15
|
|
|
14
16
|
function getCommandString(
|
|
15
17
|
printerService: PrinterService,
|
|
@@ -91,6 +93,45 @@ export async function printNoCommandSpecifiedError(context: Context) {
|
|
|
91
93
|
await printerService.error("No command specified\n\n");
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
export async function printNoCommandRecognisedError(
|
|
97
|
+
context: Context,
|
|
98
|
+
commandRegistry: CommandRegistry,
|
|
99
|
+
availableArgs: ReadonlyArray<string>,
|
|
100
|
+
) {
|
|
101
|
+
const printerService = context.getServiceById(
|
|
102
|
+
PRINTER_SERVICE_ID,
|
|
103
|
+
) as PrinterService;
|
|
104
|
+
await printerService.error("No command recognised\n\n");
|
|
105
|
+
|
|
106
|
+
if (availableArgs.length > 0) {
|
|
107
|
+
const groupCommands = commandRegistry.getGroupCommands();
|
|
108
|
+
const subCommands = commandRegistry.getSubCommands();
|
|
109
|
+
|
|
110
|
+
const allPossibleCommandNames: string[] = [];
|
|
111
|
+
for (const arg of availableArgs) {
|
|
112
|
+
// look for other possible matches
|
|
113
|
+
const possibleCommandNames = findPossibleCommandNames(
|
|
114
|
+
arg,
|
|
115
|
+
groupCommands,
|
|
116
|
+
subCommands,
|
|
117
|
+
);
|
|
118
|
+
// add the possible command names to the list if not already present
|
|
119
|
+
for (const possibleCommandName of possibleCommandNames) {
|
|
120
|
+
if (!allPossibleCommandNames.includes(possibleCommandName)) {
|
|
121
|
+
allPossibleCommandNames.push(possibleCommandName);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (allPossibleCommandNames.length > 0) {
|
|
127
|
+
await printerService.print(
|
|
128
|
+
`Possible matches: ${allPossibleCommandNames.join(", ")}\n\n`,
|
|
129
|
+
Icon.INFORMATION,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
export async function printUnusedArgsWarning(
|
|
95
136
|
context: Context,
|
|
96
137
|
overallUnusedArgs: ReadonlyArray<string>,
|
|
@@ -90,6 +90,77 @@ describe("CommandValidator tests", () => {
|
|
|
90
90
|
.toThrow();
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
test(
|
|
94
|
+
"SubCommand validation fails due to boolean argument type not supporting allowable values",
|
|
95
|
+
() => {
|
|
96
|
+
const command = getSubCommand("command", [{
|
|
97
|
+
name: "foo",
|
|
98
|
+
type: ArgumentValueTypeName.BOOLEAN,
|
|
99
|
+
allowableValues: ["goo"],
|
|
100
|
+
}], []);
|
|
101
|
+
|
|
102
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
103
|
+
.toThrow();
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
test(
|
|
108
|
+
"SubCommand validation fails due to boolean argument type not supporting case sensitivity",
|
|
109
|
+
() => {
|
|
110
|
+
const command = getSubCommand("command", [{
|
|
111
|
+
name: "foo",
|
|
112
|
+
type: ArgumentValueTypeName.BOOLEAN,
|
|
113
|
+
isCaseInsensitive: true,
|
|
114
|
+
}], []);
|
|
115
|
+
|
|
116
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
117
|
+
.toThrow();
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
test(
|
|
122
|
+
"SubCommand validation fails due to number argument type not supporting case insensitivity",
|
|
123
|
+
() => {
|
|
124
|
+
const command = getSubCommand("command", [{
|
|
125
|
+
name: "foo",
|
|
126
|
+
type: ArgumentValueTypeName.NUMBER,
|
|
127
|
+
isCaseInsensitive: false,
|
|
128
|
+
}], []);
|
|
129
|
+
|
|
130
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
131
|
+
.toThrow();
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
test(
|
|
136
|
+
"SubCommand validation fails due to string argument type not supporting value range",
|
|
137
|
+
() => {
|
|
138
|
+
const command = getSubCommand("command", [{
|
|
139
|
+
name: "foo",
|
|
140
|
+
type: ArgumentValueTypeName.STRING,
|
|
141
|
+
minValueInclusive: 1,
|
|
142
|
+
}], []);
|
|
143
|
+
|
|
144
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
145
|
+
.toThrow();
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
test(
|
|
150
|
+
"SubCommand validation fails due to number argument type not supporting value range and allowable values",
|
|
151
|
+
() => {
|
|
152
|
+
const command = getSubCommand("command", [{
|
|
153
|
+
name: "foo",
|
|
154
|
+
type: ArgumentValueTypeName.NUMBER,
|
|
155
|
+
allowableValues: [1],
|
|
156
|
+
minValueInclusive: 1,
|
|
157
|
+
}], []);
|
|
158
|
+
|
|
159
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
160
|
+
.toThrow();
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
93
164
|
test(
|
|
94
165
|
"SubCommand validation fails due to option default value" +
|
|
95
166
|
" not matching any values specified in argument valid values",
|
|
@@ -106,6 +177,38 @@ describe("CommandValidator tests", () => {
|
|
|
106
177
|
},
|
|
107
178
|
);
|
|
108
179
|
|
|
180
|
+
test(
|
|
181
|
+
"SubCommand validation fails due to option default value" +
|
|
182
|
+
" not matching any values specified in argument valid values with case sensitivity",
|
|
183
|
+
() => {
|
|
184
|
+
const command = getSubCommand("command", [{
|
|
185
|
+
name: "foo",
|
|
186
|
+
type: ArgumentValueTypeName.STRING,
|
|
187
|
+
defaultValue: "bar",
|
|
188
|
+
allowableValues: ["BAR"],
|
|
189
|
+
}], []);
|
|
190
|
+
|
|
191
|
+
expect(() => new CommandValidator(getCLIConfig()).validate(command))
|
|
192
|
+
.toThrow();
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
test(
|
|
197
|
+
"SubCommand validation successed with option default value" +
|
|
198
|
+
" matching value specified in argument valid values with case insensitivity",
|
|
199
|
+
() => {
|
|
200
|
+
const command = getSubCommand("command", [{
|
|
201
|
+
name: "foo",
|
|
202
|
+
type: ArgumentValueTypeName.STRING,
|
|
203
|
+
defaultValue: "bar",
|
|
204
|
+
allowableValues: ["BAR"],
|
|
205
|
+
isCaseInsensitive: true,
|
|
206
|
+
}], []);
|
|
207
|
+
|
|
208
|
+
new CommandValidator(getCLIConfig()).validate(command);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
109
212
|
test(
|
|
110
213
|
"SubCommand validation fails due to the type of option defaultValue" +
|
|
111
214
|
" not matching the type specified in argument type",
|
|
@@ -689,6 +689,42 @@ describe("runner tests", () => {
|
|
|
689
689
|
expect(hasRun).toBeFalse();
|
|
690
690
|
});
|
|
691
691
|
|
|
692
|
+
test("No command recognised", async () => {
|
|
693
|
+
const streamString = new StreamString();
|
|
694
|
+
let hasRun = false;
|
|
695
|
+
const option = {
|
|
696
|
+
name: "foo",
|
|
697
|
+
type: ArgumentValueTypeName.STRING,
|
|
698
|
+
shortAlias: "f",
|
|
699
|
+
};
|
|
700
|
+
const subCommand = getSubCommand("command", [option], []);
|
|
701
|
+
|
|
702
|
+
subCommand.execute = (): Promise<void> => {
|
|
703
|
+
hasRun = true;
|
|
704
|
+
return Promise.resolve();
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const commandRegistry = new DefaultCommandRegistry([subCommand]);
|
|
708
|
+
const runResult = await run(
|
|
709
|
+
["blah", "comman", "ommand"],
|
|
710
|
+
commandRegistry,
|
|
711
|
+
getServiceProviderRegistry(),
|
|
712
|
+
undefined,
|
|
713
|
+
getContext(streamString),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
expect(runResult.runState).toEqual(RunState.NO_COMMAND);
|
|
717
|
+
expectStringIncludes(
|
|
718
|
+
streamString.getString(),
|
|
719
|
+
"No command recognised",
|
|
720
|
+
);
|
|
721
|
+
expectStringIncludes(
|
|
722
|
+
streamString.getString(),
|
|
723
|
+
"Possible matches: command\n",
|
|
724
|
+
);
|
|
725
|
+
expect(hasRun).toBeFalse();
|
|
726
|
+
});
|
|
727
|
+
|
|
692
728
|
test("Unknown arg warning in non-global run", async () => {
|
|
693
729
|
const streamString = new StreamString();
|
|
694
730
|
let hasRun = false;
|
|
@@ -286,7 +286,7 @@ describe("argumentValueValidation tests", () => {
|
|
|
286
286
|
}]);
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
test("Invalid option argument value", () => {
|
|
289
|
+
test("Invalid option argument value - not an allowable value", () => {
|
|
290
290
|
let option: Option = {
|
|
291
291
|
name: "foo",
|
|
292
292
|
type: ArgumentValueTypeName.STRING,
|
|
@@ -312,6 +312,27 @@ describe("argumentValueValidation tests", () => {
|
|
|
312
312
|
}]);
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
test("Invalid option argument value - not within range", () => {
|
|
316
|
+
const option: Option = {
|
|
317
|
+
name: "foo",
|
|
318
|
+
type: ArgumentValueTypeName.INTEGER,
|
|
319
|
+
minValueInclusive: 1,
|
|
320
|
+
};
|
|
321
|
+
const invalidArguments: Array<InvalidArgument> = [];
|
|
322
|
+
expect(validateOptionValue(option, "1", invalidArguments), "1");
|
|
323
|
+
expect(invalidArguments).toEqual([]);
|
|
324
|
+
|
|
325
|
+
expect(
|
|
326
|
+
validateOptionValue(option, "0", invalidArguments),
|
|
327
|
+
).toBeUndefined();
|
|
328
|
+
expect(invalidArguments).toEqual([{
|
|
329
|
+
argument: option,
|
|
330
|
+
name: "foo",
|
|
331
|
+
value: "0",
|
|
332
|
+
reason: InvalidArgumentReason.ILLEGAL_VALUE,
|
|
333
|
+
}]);
|
|
334
|
+
});
|
|
335
|
+
|
|
315
336
|
test("Complex options", () => {
|
|
316
337
|
const option: ComplexOption = {
|
|
317
338
|
name: "foo",
|
|
@@ -122,7 +122,7 @@ describe("DefaultPrinterService tests", () => {
|
|
|
122
122
|
new TtyStyler(3),
|
|
123
123
|
);
|
|
124
124
|
printerService.colorEnabled = true;
|
|
125
|
-
await printerService.info(printerService.color("hello", "
|
|
125
|
+
await printerService.info(printerService.color("hello", "#000000"));
|
|
126
126
|
expectBytesEquals(
|
|
127
127
|
dummyStderr.getString(),
|
|
128
128
|
new Uint8Array([
|