@flowscripter/dynamic-cli-framework 1.1.5 → 1.2.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/release-bun-library.yml +1 -0
- package/README/core-commands.md +40 -0
- package/README/core-service-providers.md +198 -0
- package/README/development.md +60 -0
- package/README/implementation-details.md +474 -0
- package/README/key-concepts.md +751 -0
- package/README.md +12 -1413
- package/bun.lock +20 -18
- package/index.ts +98 -1
- package/package.json +8 -7
- package/src/api/BaseCLIFeatureOptions.ts +10 -0
- package/src/api/CLIConfig.ts +5 -0
- package/src/api/RunResult.ts +16 -0
- package/src/api/argument/Argument.ts +40 -1
- package/src/api/argument/ArgumentValueTypes.ts +1 -1
- package/src/api/command/SubCommand.ts +1 -1
- package/src/api/service/ServiceProvider.ts +3 -3
- package/src/api/service/core/ArgumentPrompterService.ts +10 -0
- package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
- package/src/api/service/core/CompletionService.ts +36 -0
- package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
- package/src/api/service/core/ImagePrinterService.ts +26 -0
- package/src/api/service/core/KeyValueService.ts +22 -5
- package/src/api/service/core/PrinterService.ts +127 -1
- package/src/api/service/core/PrompterService.ts +49 -0
- package/src/api/service/core/SecretService.ts +27 -0
- package/src/api/service/core/ShutdownService.ts +17 -1
- package/src/api/service/core/Table.ts +85 -0
- package/src/api/service/core/TableGeneratorService.ts +38 -0
- package/src/api/service/core/TreePrinterService.ts +11 -0
- package/src/cli/BaseCLI.ts +116 -44
- package/src/cli/DefaultRuntimeCLI.ts +10 -18
- package/src/command/CompletionCommand.ts +179 -0
- package/src/command/MultiCommandCliHelpCommand.ts +20 -32
- package/src/command/SingleCommandCliHelpCommand.ts +10 -1
- package/src/launcher.ts +16 -58
- package/src/runtime/command/CommandValidator.ts +70 -6
- package/src/runtime/runner.ts +112 -31
- package/src/runtime/scanner.ts +18 -15
- package/src/runtime/values/argumentValueValidation.ts +93 -10
- package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
- package/src/runtime/values/subCommandValuePopulation.ts +9 -8
- package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
- package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
- package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
- package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
- package/src/service/banner/BannerServiceProvider.ts +2 -2
- package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
- package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
- package/src/service/completion/CompletionServiceProvider.ts +212 -0
- package/src/service/completion/DefaultCompletionService.ts +364 -0
- package/src/service/completion/shell/ShellHandler.ts +16 -0
- package/src/service/completion/shell/bash.ts +47 -0
- package/src/service/completion/shell/fish.ts +43 -0
- package/src/service/completion/shell/powershell.ts +66 -0
- package/src/service/completion/shell/zsh.ts +50 -0
- package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
- package/src/service/configuration/DefaultKeyValueService.ts +53 -7
- package/src/service/configuration/DefaultSecretService.ts +85 -0
- package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
- package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
- package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
- package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
- package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
- package/src/service/printer/DefaultPrinterService.ts +147 -27
- package/src/service/printer/PrinterServiceProvider.ts +7 -7
- package/src/service/printer/command/LogLevelCommand.ts +2 -1
- package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
- package/src/service/printer/terminal/Progress.ts +21 -4
- package/src/service/printer/terminal/Spinner.ts +32 -8
- package/src/service/printer/terminal/Theme.ts +1 -1
- package/src/service/prompter/DefaultPrompterService.ts +87 -0
- package/src/service/prompter/PrompterServiceProvider.ts +36 -0
- package/src/service/prompter/command/NoPromptCommand.ts +40 -0
- package/src/service/prompter/prompt/PromptContext.ts +47 -0
- package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
- package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
- package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
- package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
- package/src/service/prompter/prompt/promptText.ts +107 -0
- package/src/service/prompter/prompt/promptToggle.ts +28 -0
- package/src/service/shutdown/DefaultShutdownService.ts +13 -0
- package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
- package/src/service/shutdown/ShutdownState.ts +5 -0
- package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
- package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
- package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
- package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
- package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
- package/src/terminal/Ansi.ts +27 -0
- package/src/terminal/ImageRenderer.ts +199 -0
- package/src/terminal/KeyReader.ts +23 -0
- package/src/terminal/PngDecoder.ts +143 -0
- package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
- package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
- package/src/terminal/TtyKeyReader.ts +83 -0
- package/src/terminal/TtyStyler.ts +78 -0
- package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
- package/src/terminal/supportsHyperlinks.ts +110 -0
- package/src/util/configHelper.ts +2 -2
- package/src/util/envVarHelper.ts +2 -2
- package/src/util/helpHelper.ts +139 -123
- package/src/util/logger.ts +16 -14
- package/src/util/runnerHelper.ts +41 -0
- package/tests/cli/BaseCLI.test.ts +50 -54
- package/tests/command/CompletionCommand.test.ts +68 -0
- package/tests/command/UsageCommand.test.ts +3 -2
- package/tests/command/VersionCommand.test.ts +3 -2
- package/tests/fixtures/Context.ts +9 -2
- package/tests/fixtures/util.ts +1 -1
- package/tests/runtime/command/CommandValidator.test.ts +103 -0
- package/tests/runtime/parser.test.ts +9 -7
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
- package/tests/runtime/runner.test.ts +36 -0
- package/tests/runtime/values/argumentValueValidation.test.ts +215 -1
- package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
- package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
- package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
- package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
- package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
- package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
- package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
- package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
- package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
- package/tests/service/completion/shell/bash.test.ts +73 -0
- package/tests/service/completion/shell/fish.test.ts +62 -0
- package/tests/service/completion/shell/powershell.test.ts +59 -0
- package/tests/service/completion/shell/zsh.test.ts +60 -0
- package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
- package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
- package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
- package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
- package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
- package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
- package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
- package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
- package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
- package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
- package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
- package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
- package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
- package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
- package/tests/service/printer/command/LogLevelCommand.test.ts +8 -6
- package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
- package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
- package/tests/service/printer/terminal/Progress.test.ts +64 -4
- package/tests/service/printer/terminal/Spinner.test.ts +44 -3
- package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
- package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
- package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
- package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
- package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
- package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
- package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
- package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
- package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
- package/tests/terminal/ImageRenderer.test.ts +161 -0
- package/tests/terminal/PngDecoder.test.ts +223 -0
- package/tests/terminal/supportsHyperlinks.test.ts +149 -0
- package/tests/util/configHelper.test.ts +4 -4
- package/tests/util/logger.test.ts +23 -1
- package/tsconfig.json +31 -0
- package/src/service/printer/terminal/Ansi.ts +0 -10
- package/src/service/printer/terminal/TtyStyler.ts +0 -39
|
@@ -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 as string).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
|
+
}
|
|
122
137
|
}
|
|
138
|
+
if (argument.minValueInclusive !== undefined) {
|
|
139
|
+
if ((convertedValue as number) < argument.minValueInclusive) {
|
|
140
|
+
return {
|
|
141
|
+
invalidArgument: {
|
|
142
|
+
argument,
|
|
143
|
+
value,
|
|
144
|
+
reason: InvalidArgumentReason.ILLEGAL_VALUE,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (argument.maxValueInclusive !== undefined) {
|
|
151
|
+
if ((convertedValue as number) > 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
|
|
|
@@ -236,7 +275,7 @@ function validateObjectValue(
|
|
|
236
275
|
const convertedObjectValue: ArgumentValues = {};
|
|
237
276
|
|
|
238
277
|
for (let i = 0; i < argument.properties.length; i++) {
|
|
239
|
-
const propertyArg = argument.properties[i]
|
|
278
|
+
const propertyArg = argument.properties[i]!;
|
|
240
279
|
const propertyValue = objectValue[propertyArg.name];
|
|
241
280
|
|
|
242
281
|
if (propertyValue === undefined) {
|
|
@@ -267,7 +306,10 @@ function validateObjectValue(
|
|
|
267
306
|
},
|
|
268
307
|
};
|
|
269
308
|
}
|
|
270
|
-
validationResult = validateArrayValue(
|
|
309
|
+
validationResult = validateArrayValue(
|
|
310
|
+
propertyArg as ComplexOption | SubCommandArgument,
|
|
311
|
+
propertyValue,
|
|
312
|
+
);
|
|
271
313
|
} else if (typeof propertyValue === "object") {
|
|
272
314
|
if (propertyArg.type !== ComplexValueTypeName.COMPLEX) {
|
|
273
315
|
return {
|
|
@@ -429,6 +471,24 @@ function doSubCommandArgumentValidation(
|
|
|
429
471
|
invalidArguments.push(validationResult.invalidArgument);
|
|
430
472
|
return undefined;
|
|
431
473
|
}
|
|
474
|
+
if (argument.validate && validationResult.validValue !== undefined) {
|
|
475
|
+
const customError = argument.validate(
|
|
476
|
+
validationResult.validValue as
|
|
477
|
+
| ArgumentValueType
|
|
478
|
+
| ArgumentValues
|
|
479
|
+
| Array<ArgumentValues>,
|
|
480
|
+
);
|
|
481
|
+
if (customError !== undefined) {
|
|
482
|
+
invalidArguments.push({
|
|
483
|
+
argument,
|
|
484
|
+
name: argument.name,
|
|
485
|
+
value: validationResult.validValue as PopulatedArgumentValueType,
|
|
486
|
+
reason: InvalidArgumentReason.CUSTOM_VALIDATION,
|
|
487
|
+
message: customError,
|
|
488
|
+
});
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
432
492
|
return validationResult.validValue as
|
|
433
493
|
| PopulatedArgumentValueType
|
|
434
494
|
| PopulatedArgumentValues
|
|
@@ -535,6 +595,24 @@ export function validateGlobalCommandArgumentValue(
|
|
|
535
595
|
return undefined;
|
|
536
596
|
}
|
|
537
597
|
|
|
598
|
+
if (
|
|
599
|
+
globalCommandArgument.validate &&
|
|
600
|
+
validationResult.validValue !== undefined
|
|
601
|
+
) {
|
|
602
|
+
const customError = globalCommandArgument.validate(
|
|
603
|
+
validationResult.validValue as ArgumentValueType,
|
|
604
|
+
);
|
|
605
|
+
if (customError !== undefined) {
|
|
606
|
+
invalidArguments.push({
|
|
607
|
+
argument: globalCommandArgument,
|
|
608
|
+
name: globalCommand.name,
|
|
609
|
+
value: validationResult.validValue,
|
|
610
|
+
reason: InvalidArgumentReason.CUSTOM_VALIDATION,
|
|
611
|
+
message: customError,
|
|
612
|
+
});
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
538
616
|
return validationResult.validValue as PopulatedArgumentSingleValueType;
|
|
539
617
|
}
|
|
540
618
|
|
|
@@ -602,6 +680,11 @@ export function getInvalidArgumentString(
|
|
|
602
680
|
case InvalidArgumentReason.OPTION_IS_COMPLEX:
|
|
603
681
|
invalidString = "(specified option is complex)";
|
|
604
682
|
break;
|
|
683
|
+
case InvalidArgumentReason.CUSTOM_VALIDATION:
|
|
684
|
+
invalidString = invalidArgument.message
|
|
685
|
+
? `(custom validation: ${invalidArgument.message})`
|
|
686
|
+
: "(custom validation failed)";
|
|
687
|
+
break;
|
|
605
688
|
default:
|
|
606
689
|
invalidString = "";
|
|
607
690
|
break;
|
|
@@ -49,7 +49,7 @@ export default function populateGlobalCommandValue(
|
|
|
49
49
|
|
|
50
50
|
// don't use potentialArg if it cannot be a boolean value...
|
|
51
51
|
if (argument.type === ArgumentValueTypeName.BOOLEAN) {
|
|
52
|
-
const firstPotentialArgLower = firstPotentialArg
|
|
52
|
+
const firstPotentialArgLower = firstPotentialArg!.toLowerCase();
|
|
53
53
|
|
|
54
54
|
if (
|
|
55
55
|
(firstPotentialArgLower !== "true") &&
|
|
@@ -175,14 +175,14 @@ class ParseContext {
|
|
|
175
175
|
|
|
176
176
|
for (let i = 0; i < complexPathElements.length; i++) {
|
|
177
177
|
let arrayIndexString: string | undefined;
|
|
178
|
-
let complexPathElement = complexPathElements[i]
|
|
178
|
+
let complexPathElement = complexPathElements[i]!;
|
|
179
179
|
|
|
180
180
|
// look for array index
|
|
181
181
|
if (complexPathElement.endsWith("]")) {
|
|
182
182
|
[complexPathElement, arrayIndexString] = complexPathElement.slice(
|
|
183
183
|
0,
|
|
184
184
|
complexPathElement.length - 1,
|
|
185
|
-
).split("[");
|
|
185
|
+
).split("[") as [string, string | undefined];
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
let lookupPath = this.subCommand.name;
|
|
@@ -237,7 +237,7 @@ class ParseContext {
|
|
|
237
237
|
}
|
|
238
238
|
option = this.optionLookupMapsByPath.get(lookupPath)!.get(
|
|
239
239
|
complexPathElement,
|
|
240
|
-
);
|
|
240
|
+
) as Option | ComplexOption | undefined;
|
|
241
241
|
|
|
242
242
|
// now we are parsing properties of complex options it is an error if we cannot match an option name or alias
|
|
243
243
|
if (option === undefined) {
|
|
@@ -423,8 +423,8 @@ class ParseContext {
|
|
|
423
423
|
if (typeof pathElement === "string") {
|
|
424
424
|
if (Array.isArray(optionValue)) {
|
|
425
425
|
optionValue = (optionValue as Array<PopulatedArgumentValues>)[
|
|
426
|
-
optionValue.length - 1
|
|
427
|
-
][pathElement as string] as
|
|
426
|
+
(optionValue as Array<PopulatedArgumentValues>).length - 1
|
|
427
|
+
]![pathElement as string] as
|
|
428
428
|
| Array<PopulatedArgumentValues>
|
|
429
429
|
| PopulatedArgumentValues
|
|
430
430
|
| PopulatedArgumentValueType;
|
|
@@ -459,7 +459,8 @@ class ParseContext {
|
|
|
459
459
|
* @param value the value to attempt to set.
|
|
460
460
|
*/
|
|
461
461
|
setPositionalValue(value: ArgumentSingleValueType): boolean {
|
|
462
|
-
const positional = this.subCommand
|
|
462
|
+
const positional = this.subCommand
|
|
463
|
+
.positionals[this.currentPositionalIndex]!;
|
|
463
464
|
const currentValue = this.populatedArgumentValues[positional.name];
|
|
464
465
|
|
|
465
466
|
// if we haven't populated the argument value before...
|
|
@@ -536,7 +537,7 @@ class ParseContext {
|
|
|
536
537
|
// check for option AND value
|
|
537
538
|
if (potentialOptionPath.includes("=")) {
|
|
538
539
|
[potentialOptionPath, potentialOptionValue] = potentialOptionPath
|
|
539
|
-
.split(/=(.*)/);
|
|
540
|
+
.split(/=(.*)/) as [string, string | undefined, ...string[]];
|
|
540
541
|
|
|
541
542
|
// check for illegal syntax e.g. --optionName=
|
|
542
543
|
if (potentialOptionValue === "") {
|
|
@@ -623,7 +624,7 @@ export default function populateSubCommandValues(
|
|
|
623
624
|
const unusedArgs: Array<string> = [];
|
|
624
625
|
|
|
625
626
|
for (let i = 0; i < potentialArgs.length; i++) {
|
|
626
|
-
const potentialArg = potentialArgs[i]
|
|
627
|
+
const potentialArg = potentialArgs[i]!;
|
|
627
628
|
|
|
628
629
|
// parse the next arg
|
|
629
630
|
parseContext.parse(potentialArg);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type Context from "../../api/Context.ts";
|
|
2
|
+
import type {
|
|
3
|
+
ServiceInfo,
|
|
4
|
+
ServiceProvider,
|
|
5
|
+
} from "../../api/service/ServiceProvider.ts";
|
|
6
|
+
import type ArgumentPrompterService from "../../api/service/core/ArgumentPrompterService.ts";
|
|
7
|
+
import { ARGUMENT_PROMPTER_SERVICE_ID } from "../../api/service/core/ArgumentPrompterService.ts";
|
|
8
|
+
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
9
|
+
|
|
10
|
+
export default class ArgumentPrompterServiceProvider
|
|
11
|
+
implements ServiceProvider {
|
|
12
|
+
readonly serviceId: string = ARGUMENT_PROMPTER_SERVICE_ID;
|
|
13
|
+
readonly servicePriority: number;
|
|
14
|
+
readonly argumentPrompterService: ArgumentPrompterService;
|
|
15
|
+
|
|
16
|
+
public constructor(
|
|
17
|
+
servicePriority: number,
|
|
18
|
+
argumentPrompterService: ArgumentPrompterService,
|
|
19
|
+
) {
|
|
20
|
+
this.servicePriority = servicePriority;
|
|
21
|
+
this.argumentPrompterService = argumentPrompterService;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
service: this.argumentPrompterService,
|
|
27
|
+
commands: [],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
initService(_context: Context): Promise<void> {
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type ArgumentPrompterService from "../../api/service/core/ArgumentPrompterService.ts";
|
|
2
|
+
import type { ParseResult } from "../../runtime/parser.ts";
|
|
3
|
+
import type PrompterService from "../../api/service/core/PrompterService.ts";
|
|
4
|
+
import {
|
|
5
|
+
type Prompt,
|
|
6
|
+
type PromptOption,
|
|
7
|
+
PromptType,
|
|
8
|
+
} from "../../api/service/core/PrompterService.ts";
|
|
9
|
+
import { InvalidArgumentReason } from "../../api/RunResult.ts";
|
|
10
|
+
import type { InvalidArgument } from "../../api/RunResult.ts";
|
|
11
|
+
import {
|
|
12
|
+
type ArgumentSingleValueType,
|
|
13
|
+
ArgumentValueTypeName,
|
|
14
|
+
ComplexValueTypeName,
|
|
15
|
+
type PopulatedArgumentSingleValueType,
|
|
16
|
+
type PopulatedArgumentValues,
|
|
17
|
+
} from "../../api/argument/ArgumentValueTypes.ts";
|
|
18
|
+
import type Argument from "../../api/argument/Argument.ts";
|
|
19
|
+
import type Option from "../../api/argument/Option.ts";
|
|
20
|
+
import type Positional from "../../api/argument/Positional.ts";
|
|
21
|
+
import type ComplexOption from "../../api/argument/ComplexOption.ts";
|
|
22
|
+
import type SubCommand from "../../api/command/SubCommand.ts";
|
|
23
|
+
import type GlobalCommand from "../../api/command/GlobalCommand.ts";
|
|
24
|
+
import {
|
|
25
|
+
isGlobalCommand,
|
|
26
|
+
isSubCommand,
|
|
27
|
+
} from "../../runtime/command/CommandTypeGuards.ts";
|
|
28
|
+
|
|
29
|
+
export default class DefaultArgumentPrompterService
|
|
30
|
+
implements ArgumentPrompterService {
|
|
31
|
+
readonly #prompterService: PrompterService;
|
|
32
|
+
|
|
33
|
+
public constructor(prompterService: PrompterService) {
|
|
34
|
+
this.#prompterService = prompterService;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async promptForMissingArguments(
|
|
38
|
+
parseResult: ParseResult,
|
|
39
|
+
): Promise<ParseResult> {
|
|
40
|
+
if (!this.#prompterService.promptEnabled) {
|
|
41
|
+
return parseResult;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const allMissingValue = parseResult.invalidArguments.every(
|
|
45
|
+
(ia) => ia.reason === InvalidArgumentReason.MISSING_VALUE,
|
|
46
|
+
);
|
|
47
|
+
if (!allMissingValue) {
|
|
48
|
+
return parseResult;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parseResult.invalidArguments.length === 0) {
|
|
52
|
+
return parseResult;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (isGlobalCommand(parseResult.command)) {
|
|
57
|
+
return await this.#promptForGlobalCommand(parseResult);
|
|
58
|
+
}
|
|
59
|
+
if (isSubCommand(parseResult.command)) {
|
|
60
|
+
return await this.#promptForSubCommand(parseResult);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as Error).message === "Interrupted") {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return parseResult;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return parseResult;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async #promptForGlobalCommand(
|
|
73
|
+
parseResult: ParseResult,
|
|
74
|
+
): Promise<ParseResult> {
|
|
75
|
+
const command = parseResult.command as GlobalCommand;
|
|
76
|
+
if (!command.argument) {
|
|
77
|
+
return parseResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
|
|
81
|
+
command.name,
|
|
82
|
+
command.name,
|
|
83
|
+
command.argument,
|
|
84
|
+
);
|
|
85
|
+
const result = await this.#prompterService.prompt(prompt);
|
|
86
|
+
const value = DefaultArgumentPrompterService.#coerceValue(
|
|
87
|
+
result.value as ArgumentSingleValueType,
|
|
88
|
+
command.argument.type,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
command: parseResult.command,
|
|
93
|
+
groupCommand: parseResult.groupCommand,
|
|
94
|
+
populatedArgumentValues: value as PopulatedArgumentSingleValueType,
|
|
95
|
+
invalidArguments: [],
|
|
96
|
+
unusedArgs: parseResult.unusedArgs,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #promptForSubCommand(
|
|
101
|
+
parseResult: ParseResult,
|
|
102
|
+
): Promise<ParseResult> {
|
|
103
|
+
const command = parseResult.command as SubCommand;
|
|
104
|
+
const existingValues = (parseResult.populatedArgumentValues ??
|
|
105
|
+
{}) as PopulatedArgumentValues;
|
|
106
|
+
const newValues: Record<
|
|
107
|
+
string,
|
|
108
|
+
PopulatedArgumentValues[string]
|
|
109
|
+
> = { ...existingValues };
|
|
110
|
+
const remainingInvalid: InvalidArgument[] = [];
|
|
111
|
+
|
|
112
|
+
for (const invalid of parseResult.invalidArguments) {
|
|
113
|
+
if (
|
|
114
|
+
invalid.reason !== InvalidArgumentReason.MISSING_VALUE ||
|
|
115
|
+
!invalid.name
|
|
116
|
+
) {
|
|
117
|
+
remainingInvalid.push(invalid);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const arg = DefaultArgumentPrompterService.#findArgument(
|
|
122
|
+
command,
|
|
123
|
+
invalid.name,
|
|
124
|
+
);
|
|
125
|
+
if (!arg) {
|
|
126
|
+
remainingInvalid.push(invalid);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
"type" in arg &&
|
|
132
|
+
(arg as ComplexOption).type === ComplexValueTypeName.COMPLEX
|
|
133
|
+
) {
|
|
134
|
+
const complexValues = await this.#promptForComplexOption(
|
|
135
|
+
invalid.name,
|
|
136
|
+
arg as ComplexOption,
|
|
137
|
+
);
|
|
138
|
+
newValues[invalid.name] = complexValues;
|
|
139
|
+
} else if (
|
|
140
|
+
DefaultArgumentPrompterService.#isArrayArgument(arg)
|
|
141
|
+
) {
|
|
142
|
+
const arrayValues = await this.#promptForArrayArgument(
|
|
143
|
+
invalid.name,
|
|
144
|
+
arg as Option | Positional,
|
|
145
|
+
);
|
|
146
|
+
newValues[invalid.name] = arrayValues;
|
|
147
|
+
} else {
|
|
148
|
+
const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
|
|
149
|
+
invalid.name,
|
|
150
|
+
(arg as Option | Positional).description ?? invalid.name,
|
|
151
|
+
arg as Argument,
|
|
152
|
+
);
|
|
153
|
+
const result = await this.#prompterService.prompt(prompt);
|
|
154
|
+
newValues[invalid.name] = DefaultArgumentPrompterService.#coerceValue(
|
|
155
|
+
result.value as ArgumentSingleValueType,
|
|
156
|
+
(arg as Argument).type,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
command: parseResult.command,
|
|
163
|
+
groupCommand: parseResult.groupCommand,
|
|
164
|
+
populatedArgumentValues: newValues as PopulatedArgumentValues,
|
|
165
|
+
invalidArguments: remainingInvalid,
|
|
166
|
+
unusedArgs: parseResult.unusedArgs,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async #promptForComplexOption(
|
|
171
|
+
name: string,
|
|
172
|
+
complexOption: ComplexOption,
|
|
173
|
+
): Promise<PopulatedArgumentValues> {
|
|
174
|
+
const values: Record<string, PopulatedArgumentValues[string]> = {};
|
|
175
|
+
|
|
176
|
+
for (const prop of complexOption.properties) {
|
|
177
|
+
if (
|
|
178
|
+
"type" in prop &&
|
|
179
|
+
(prop as ComplexOption).type === ComplexValueTypeName.COMPLEX
|
|
180
|
+
) {
|
|
181
|
+
values[(prop as ComplexOption).name] = await this
|
|
182
|
+
.#promptForComplexOption(
|
|
183
|
+
`${name}.${(prop as ComplexOption).name}`,
|
|
184
|
+
prop as ComplexOption,
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
const option = prop as Option;
|
|
188
|
+
if (option.isOptional && option.defaultValue !== undefined) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
|
|
192
|
+
`${name}.${option.name}`,
|
|
193
|
+
option.description ?? `${name}.${option.name}`,
|
|
194
|
+
option,
|
|
195
|
+
);
|
|
196
|
+
const result = await this.#prompterService.prompt(prompt);
|
|
197
|
+
values[option.name] = DefaultArgumentPrompterService.#coerceValue(
|
|
198
|
+
result.value as ArgumentSingleValueType,
|
|
199
|
+
option.type,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return values as PopulatedArgumentValues;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async #promptForArrayArgument(
|
|
208
|
+
name: string,
|
|
209
|
+
arg: Option | Positional,
|
|
210
|
+
): Promise<Array<ArgumentSingleValueType>> {
|
|
211
|
+
const values: ArgumentSingleValueType[] = [];
|
|
212
|
+
|
|
213
|
+
while (true) {
|
|
214
|
+
const prompt = DefaultArgumentPrompterService.#argumentToPrompt(
|
|
215
|
+
name,
|
|
216
|
+
`${arg.description ?? name}${
|
|
217
|
+
values.length > 0 ? ` [${values.length + 1}]` : ""
|
|
218
|
+
}`,
|
|
219
|
+
arg as Argument,
|
|
220
|
+
);
|
|
221
|
+
const result = await this.#prompterService.prompt(prompt);
|
|
222
|
+
values.push(
|
|
223
|
+
DefaultArgumentPrompterService.#coerceValue(
|
|
224
|
+
result.value as ArgumentSingleValueType,
|
|
225
|
+
(arg as Argument).type,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const addAnother: Prompt = {
|
|
230
|
+
name: `${name}_more`,
|
|
231
|
+
promptText: `Add another value for ${name}?`,
|
|
232
|
+
type: PromptType.ACKNOWLEDGE,
|
|
233
|
+
options: [],
|
|
234
|
+
};
|
|
235
|
+
const moreResult = await this.#prompterService.prompt(addAnother);
|
|
236
|
+
if (moreResult.value !== true) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return values;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static #argumentToPrompt(
|
|
245
|
+
name: string,
|
|
246
|
+
description: string,
|
|
247
|
+
arg: Argument,
|
|
248
|
+
): Prompt {
|
|
249
|
+
if (arg.type === ArgumentValueTypeName.BOOLEAN) {
|
|
250
|
+
return {
|
|
251
|
+
name,
|
|
252
|
+
promptText: description,
|
|
253
|
+
type: PromptType.TOGGLE,
|
|
254
|
+
options: [],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (arg.allowableValues && arg.allowableValues.length > 0) {
|
|
259
|
+
const options: PromptOption[] = arg.allowableValues.map((v) => ({
|
|
260
|
+
displayValue: String(v),
|
|
261
|
+
returnedValue: v,
|
|
262
|
+
}));
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
promptText: description,
|
|
266
|
+
type: PromptType.SINGLE_SELECT,
|
|
267
|
+
options,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const validationOption: PromptOption = {
|
|
272
|
+
displayValue: arg.type === ArgumentValueTypeName.SECRET
|
|
273
|
+
? "__SECRET__"
|
|
274
|
+
: name,
|
|
275
|
+
returnedValue: "",
|
|
276
|
+
min: arg.minValueInclusive,
|
|
277
|
+
max: arg.maxValueInclusive,
|
|
278
|
+
validate: arg.validate ? (v) => arg.validate!(v) : undefined,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
name,
|
|
283
|
+
promptText: description,
|
|
284
|
+
type: PromptType.TEXT,
|
|
285
|
+
options: [validationOption],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static #coerceValue(
|
|
290
|
+
value: ArgumentSingleValueType,
|
|
291
|
+
type: ArgumentValueTypeName,
|
|
292
|
+
): ArgumentSingleValueType {
|
|
293
|
+
if (typeof value === "string") {
|
|
294
|
+
switch (type) {
|
|
295
|
+
case ArgumentValueTypeName.INTEGER:
|
|
296
|
+
return parseInt(value, 10);
|
|
297
|
+
case ArgumentValueTypeName.NUMBER:
|
|
298
|
+
return parseFloat(value);
|
|
299
|
+
case ArgumentValueTypeName.BOOLEAN:
|
|
300
|
+
return value.toLowerCase() === "true";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static #findArgument(
|
|
307
|
+
command: SubCommand,
|
|
308
|
+
name: string,
|
|
309
|
+
): Option | Positional | ComplexOption | undefined {
|
|
310
|
+
for (const opt of command.options) {
|
|
311
|
+
if (opt.name === name) return opt;
|
|
312
|
+
}
|
|
313
|
+
for (const pos of command.positionals) {
|
|
314
|
+
if (pos.name === name) return pos;
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
static #isArrayArgument(
|
|
320
|
+
arg: Option | Positional | ComplexOption,
|
|
321
|
+
): boolean {
|
|
322
|
+
if ("isArray" in arg && (arg as Option).isArray) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
if ("isVarargMultiple" in arg && (arg as Positional).isVarargMultiple) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -32,7 +32,7 @@ export default class AsciiBannerGeneratorServiceProvider
|
|
|
32
32
|
new DefaultAsciiBannerGeneratorService();
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
public
|
|
35
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
36
36
|
return Promise.resolve({
|
|
37
37
|
service: this.#asciiBannerGeneratorService,
|
|
38
38
|
commands: [],
|