@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.
Files changed (165) hide show
  1. package/.github/workflows/release-bun-library.yml +1 -0
  2. package/README/core-commands.md +40 -0
  3. package/README/core-service-providers.md +198 -0
  4. package/README/development.md +60 -0
  5. package/README/implementation-details.md +474 -0
  6. package/README/key-concepts.md +751 -0
  7. package/README.md +12 -1413
  8. package/bun.lock +20 -18
  9. package/index.ts +98 -1
  10. package/package.json +8 -7
  11. package/src/api/BaseCLIFeatureOptions.ts +10 -0
  12. package/src/api/CLIConfig.ts +5 -0
  13. package/src/api/RunResult.ts +16 -0
  14. package/src/api/argument/Argument.ts +40 -1
  15. package/src/api/argument/ArgumentValueTypes.ts +1 -1
  16. package/src/api/command/SubCommand.ts +1 -1
  17. package/src/api/service/ServiceProvider.ts +3 -3
  18. package/src/api/service/core/ArgumentPrompterService.ts +10 -0
  19. package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
  20. package/src/api/service/core/CompletionService.ts +36 -0
  21. package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
  22. package/src/api/service/core/ImagePrinterService.ts +26 -0
  23. package/src/api/service/core/KeyValueService.ts +22 -5
  24. package/src/api/service/core/PrinterService.ts +127 -1
  25. package/src/api/service/core/PrompterService.ts +49 -0
  26. package/src/api/service/core/SecretService.ts +27 -0
  27. package/src/api/service/core/ShutdownService.ts +17 -1
  28. package/src/api/service/core/Table.ts +85 -0
  29. package/src/api/service/core/TableGeneratorService.ts +38 -0
  30. package/src/api/service/core/TreePrinterService.ts +11 -0
  31. package/src/cli/BaseCLI.ts +116 -44
  32. package/src/cli/DefaultRuntimeCLI.ts +10 -18
  33. package/src/command/CompletionCommand.ts +179 -0
  34. package/src/command/MultiCommandCliHelpCommand.ts +20 -32
  35. package/src/command/SingleCommandCliHelpCommand.ts +10 -1
  36. package/src/launcher.ts +16 -58
  37. package/src/runtime/command/CommandValidator.ts +70 -6
  38. package/src/runtime/runner.ts +112 -31
  39. package/src/runtime/scanner.ts +18 -15
  40. package/src/runtime/values/argumentValueValidation.ts +93 -10
  41. package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
  42. package/src/runtime/values/subCommandValuePopulation.ts +9 -8
  43. package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
  44. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
  45. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
  46. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
  47. package/src/service/banner/BannerServiceProvider.ts +2 -2
  48. package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
  49. package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
  50. package/src/service/completion/CompletionServiceProvider.ts +212 -0
  51. package/src/service/completion/DefaultCompletionService.ts +364 -0
  52. package/src/service/completion/shell/ShellHandler.ts +16 -0
  53. package/src/service/completion/shell/bash.ts +47 -0
  54. package/src/service/completion/shell/fish.ts +43 -0
  55. package/src/service/completion/shell/powershell.ts +66 -0
  56. package/src/service/completion/shell/zsh.ts +50 -0
  57. package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
  58. package/src/service/configuration/DefaultKeyValueService.ts +53 -7
  59. package/src/service/configuration/DefaultSecretService.ts +85 -0
  60. package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
  61. package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
  62. package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
  63. package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
  64. package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
  65. package/src/service/printer/DefaultPrinterService.ts +147 -27
  66. package/src/service/printer/PrinterServiceProvider.ts +7 -7
  67. package/src/service/printer/command/LogLevelCommand.ts +2 -1
  68. package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
  69. package/src/service/printer/terminal/Progress.ts +21 -4
  70. package/src/service/printer/terminal/Spinner.ts +32 -8
  71. package/src/service/printer/terminal/Theme.ts +1 -1
  72. package/src/service/prompter/DefaultPrompterService.ts +87 -0
  73. package/src/service/prompter/PrompterServiceProvider.ts +36 -0
  74. package/src/service/prompter/command/NoPromptCommand.ts +40 -0
  75. package/src/service/prompter/prompt/PromptContext.ts +47 -0
  76. package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
  77. package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
  78. package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
  79. package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
  80. package/src/service/prompter/prompt/promptText.ts +107 -0
  81. package/src/service/prompter/prompt/promptToggle.ts +28 -0
  82. package/src/service/shutdown/DefaultShutdownService.ts +13 -0
  83. package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
  84. package/src/service/shutdown/ShutdownState.ts +5 -0
  85. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
  86. package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
  87. package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
  88. package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
  89. package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
  90. package/src/terminal/Ansi.ts +27 -0
  91. package/src/terminal/ImageRenderer.ts +199 -0
  92. package/src/terminal/KeyReader.ts +23 -0
  93. package/src/terminal/PngDecoder.ts +143 -0
  94. package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
  95. package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
  96. package/src/terminal/TtyKeyReader.ts +83 -0
  97. package/src/terminal/TtyStyler.ts +78 -0
  98. package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
  99. package/src/terminal/supportsHyperlinks.ts +110 -0
  100. package/src/util/configHelper.ts +2 -2
  101. package/src/util/envVarHelper.ts +2 -2
  102. package/src/util/helpHelper.ts +139 -123
  103. package/src/util/logger.ts +16 -14
  104. package/src/util/runnerHelper.ts +41 -0
  105. package/tests/cli/BaseCLI.test.ts +50 -54
  106. package/tests/command/CompletionCommand.test.ts +68 -0
  107. package/tests/command/UsageCommand.test.ts +3 -2
  108. package/tests/command/VersionCommand.test.ts +3 -2
  109. package/tests/fixtures/Context.ts +9 -2
  110. package/tests/fixtures/util.ts +1 -1
  111. package/tests/runtime/command/CommandValidator.test.ts +103 -0
  112. package/tests/runtime/parser.test.ts +9 -7
  113. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
  114. package/tests/runtime/runner.test.ts +36 -0
  115. package/tests/runtime/values/argumentValueValidation.test.ts +215 -1
  116. package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
  117. package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
  118. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
  119. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
  120. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
  121. package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
  122. package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
  123. package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
  124. package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
  125. package/tests/service/completion/shell/bash.test.ts +73 -0
  126. package/tests/service/completion/shell/fish.test.ts +62 -0
  127. package/tests/service/completion/shell/powershell.test.ts +59 -0
  128. package/tests/service/completion/shell/zsh.test.ts +60 -0
  129. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
  130. package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
  131. package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
  132. package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
  133. package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
  134. package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
  135. package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
  136. package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
  137. package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
  138. package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
  139. package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
  140. package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
  141. package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
  142. package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
  143. package/tests/service/printer/command/LogLevelCommand.test.ts +8 -6
  144. package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
  145. package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
  146. package/tests/service/printer/terminal/Progress.test.ts +64 -4
  147. package/tests/service/printer/terminal/Spinner.test.ts +44 -3
  148. package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
  149. package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
  150. package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
  151. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
  152. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
  153. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
  154. package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
  155. package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
  156. package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
  157. package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
  158. package/tests/terminal/ImageRenderer.test.ts +161 -0
  159. package/tests/terminal/PngDecoder.test.ts +223 -0
  160. package/tests/terminal/supportsHyperlinks.test.ts +149 -0
  161. package/tests/util/configHelper.test.ts +4 -4
  162. package/tests/util/logger.test.ts +23 -1
  163. package/tsconfig.json +31 -0
  164. package/src/service/printer/terminal/Ansi.ts +0 -10
  165. 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
- if (argument.allowableValues && !argument.allowableValues.includes(value)) {
115
- return {
116
- invalidArgument: {
117
- argument,
118
- value,
119
- reason: InvalidArgumentReason.ILLEGAL_VALUE,
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(propertyArg, propertyValue);
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.toLowerCase();
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.positionals[this.currentPositionalIndex];
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 provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
35
+ public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
36
36
  return Promise.resolve({
37
37
  service: this.#asciiBannerGeneratorService,
38
38
  commands: [],