@flowscripter/dynamic-cli-framework 1.1.6 → 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 (160) 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 +97 -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 +24 -0
  15. package/src/api/service/ServiceProvider.ts +3 -3
  16. package/src/api/service/core/ArgumentPrompterService.ts +10 -0
  17. package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
  18. package/src/api/service/core/CompletionService.ts +36 -0
  19. package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
  20. package/src/api/service/core/ImagePrinterService.ts +26 -0
  21. package/src/api/service/core/KeyValueService.ts +22 -5
  22. package/src/api/service/core/PrinterService.ts +127 -1
  23. package/src/api/service/core/PrompterService.ts +49 -0
  24. package/src/api/service/core/SecretService.ts +27 -0
  25. package/src/api/service/core/ShutdownService.ts +17 -1
  26. package/src/api/service/core/Table.ts +85 -0
  27. package/src/api/service/core/TableGeneratorService.ts +38 -0
  28. package/src/api/service/core/TreePrinterService.ts +11 -0
  29. package/src/cli/BaseCLI.ts +116 -44
  30. package/src/cli/DefaultRuntimeCLI.ts +10 -18
  31. package/src/command/CompletionCommand.ts +179 -0
  32. package/src/command/MultiCommandCliHelpCommand.ts +18 -2
  33. package/src/command/SingleCommandCliHelpCommand.ts +10 -1
  34. package/src/launcher.ts +16 -58
  35. package/src/runtime/command/CommandValidator.ts +3 -3
  36. package/src/runtime/runner.ts +102 -30
  37. package/src/runtime/scanner.ts +18 -15
  38. package/src/runtime/values/argumentValueValidation.ts +49 -5
  39. package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
  40. package/src/runtime/values/subCommandValuePopulation.ts +9 -8
  41. package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
  42. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
  43. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
  44. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
  45. package/src/service/banner/BannerServiceProvider.ts +2 -2
  46. package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
  47. package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
  48. package/src/service/completion/CompletionServiceProvider.ts +212 -0
  49. package/src/service/completion/DefaultCompletionService.ts +364 -0
  50. package/src/service/completion/shell/ShellHandler.ts +16 -0
  51. package/src/service/completion/shell/bash.ts +47 -0
  52. package/src/service/completion/shell/fish.ts +43 -0
  53. package/src/service/completion/shell/powershell.ts +66 -0
  54. package/src/service/completion/shell/zsh.ts +50 -0
  55. package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
  56. package/src/service/configuration/DefaultKeyValueService.ts +53 -7
  57. package/src/service/configuration/DefaultSecretService.ts +85 -0
  58. package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
  59. package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
  60. package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
  61. package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
  62. package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
  63. package/src/service/printer/DefaultPrinterService.ts +147 -27
  64. package/src/service/printer/PrinterServiceProvider.ts +7 -7
  65. package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
  66. package/src/service/printer/terminal/Progress.ts +21 -4
  67. package/src/service/printer/terminal/Spinner.ts +32 -8
  68. package/src/service/printer/terminal/Theme.ts +1 -1
  69. package/src/service/prompter/DefaultPrompterService.ts +87 -0
  70. package/src/service/prompter/PrompterServiceProvider.ts +36 -0
  71. package/src/service/prompter/command/NoPromptCommand.ts +40 -0
  72. package/src/service/prompter/prompt/PromptContext.ts +47 -0
  73. package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
  74. package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
  75. package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
  76. package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
  77. package/src/service/prompter/prompt/promptText.ts +107 -0
  78. package/src/service/prompter/prompt/promptToggle.ts +28 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +13 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
  81. package/src/service/shutdown/ShutdownState.ts +5 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
  83. package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
  84. package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
  85. package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
  86. package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
  87. package/src/terminal/Ansi.ts +27 -0
  88. package/src/terminal/ImageRenderer.ts +199 -0
  89. package/src/terminal/KeyReader.ts +23 -0
  90. package/src/terminal/PngDecoder.ts +143 -0
  91. package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
  92. package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
  93. package/src/terminal/TtyKeyReader.ts +83 -0
  94. package/src/terminal/TtyStyler.ts +78 -0
  95. package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
  96. package/src/terminal/supportsHyperlinks.ts +110 -0
  97. package/src/util/configHelper.ts +2 -2
  98. package/src/util/envVarHelper.ts +2 -2
  99. package/src/util/helpHelper.ts +66 -122
  100. package/src/util/logger.ts +16 -14
  101. package/src/util/runnerHelper.ts +1 -1
  102. package/tests/cli/BaseCLI.test.ts +50 -54
  103. package/tests/command/CompletionCommand.test.ts +68 -0
  104. package/tests/command/UsageCommand.test.ts +3 -2
  105. package/tests/command/VersionCommand.test.ts +3 -2
  106. package/tests/fixtures/Context.ts +9 -2
  107. package/tests/fixtures/util.ts +1 -1
  108. package/tests/runtime/parser.test.ts +9 -7
  109. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
  110. package/tests/runtime/values/argumentValueValidation.test.ts +193 -0
  111. package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
  112. package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
  113. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
  114. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
  115. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
  116. package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
  117. package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
  118. package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
  119. package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
  120. package/tests/service/completion/shell/bash.test.ts +73 -0
  121. package/tests/service/completion/shell/fish.test.ts +62 -0
  122. package/tests/service/completion/shell/powershell.test.ts +59 -0
  123. package/tests/service/completion/shell/zsh.test.ts +60 -0
  124. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
  125. package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
  126. package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
  127. package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
  128. package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
  129. package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
  130. package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
  131. package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
  132. package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
  133. package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
  134. package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
  135. package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
  136. package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
  137. package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
  138. package/tests/service/printer/command/LogLevelCommand.test.ts +7 -5
  139. package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
  140. package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
  141. package/tests/service/printer/terminal/Progress.test.ts +64 -4
  142. package/tests/service/printer/terminal/Spinner.test.ts +44 -3
  143. package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
  144. package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
  145. package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
  146. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
  147. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
  148. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
  149. package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
  150. package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
  151. package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
  152. package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
  153. package/tests/terminal/ImageRenderer.test.ts +161 -0
  154. package/tests/terminal/PngDecoder.test.ts +223 -0
  155. package/tests/terminal/supportsHyperlinks.test.ts +149 -0
  156. package/tests/util/configHelper.test.ts +4 -4
  157. package/tests/util/logger.test.ts +23 -1
  158. package/tsconfig.json +31 -0
  159. package/src/service/printer/terminal/Ansi.ts +0 -10
  160. package/src/service/printer/terminal/TtyStyler.ts +0 -39
@@ -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: [],
@@ -1,12 +1,94 @@
1
1
  import type AsciiBannerGeneratorService from "../../api/service/core/AsciiBannerGeneratorService.ts";
2
+ import type {
3
+ BannerGenerateOptions,
4
+ ColorEffect,
5
+ } from "../../api/service/core/AsciiBannerGeneratorService.ts";
6
+ import {
7
+ BACKGROUND_COLOR_END,
8
+ backgroundColorStart,
9
+ FOREGROUND_COLOR_END,
10
+ foregroundColorStart,
11
+ } from "../../terminal/Ansi.ts";
2
12
  import figlet from "figlet";
3
- // FIGlet font is converted to a JSON string and embedded in a simple JSON file: `{ "font": "<figlet font definition>" }`
4
13
  import standardFont from "./standard.flf.json" with { type: "json" };
5
14
 
6
- /**
7
- * Default implementation of {@link AsciiBannerGeneratorService} which has a font definition
8
- * for the FIGlet "standard" font already registered.
9
- */
15
+ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
16
+ const c = (1 - Math.abs(2 * l - 1)) * s;
17
+ const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
18
+ const m = l - c / 2;
19
+ let r = 0, g = 0, b = 0;
20
+ if (h < 1 / 6) {
21
+ r = c;
22
+ g = x;
23
+ b = 0;
24
+ } else if (h < 2 / 6) {
25
+ r = x;
26
+ g = c;
27
+ b = 0;
28
+ } else if (h < 3 / 6) {
29
+ r = 0;
30
+ g = c;
31
+ b = x;
32
+ } else if (h < 4 / 6) {
33
+ r = 0;
34
+ g = x;
35
+ b = c;
36
+ } else if (h < 5 / 6) {
37
+ r = x;
38
+ g = 0;
39
+ b = c;
40
+ } else {
41
+ r = c;
42
+ g = 0;
43
+ b = x;
44
+ }
45
+ return [
46
+ Math.round((r + m) * 255),
47
+ Math.round((g + m) * 255),
48
+ Math.round((b + m) * 255),
49
+ ];
50
+ }
51
+
52
+ function parseHex(hex: string): [number, number, number] {
53
+ const v = parseInt(hex.slice(1), 16);
54
+ return [(v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
55
+ }
56
+
57
+ function interpolateColors(
58
+ colorStops: [number, number, number][],
59
+ position: number,
60
+ total: number,
61
+ ): [number, number, number] {
62
+ const first = colorStops[0];
63
+ if (total <= 1 || first === undefined) return first ?? [0, 0, 0];
64
+ const t = position / (total - 1);
65
+ const scaledT = t * (colorStops.length - 1);
66
+ const idx = Math.min(Math.floor(scaledT), colorStops.length - 2);
67
+ const localT = scaledT - idx;
68
+ const stop1 = colorStops[idx];
69
+ const stop2 = colorStops[idx + 1];
70
+ if (stop1 === undefined || stop2 === undefined) return first;
71
+ const [r1, g1, b1] = stop1;
72
+ const [r2, g2, b2] = stop2;
73
+ return [
74
+ Math.round(r1 + (r2 - r1) * localT),
75
+ Math.round(g1 + (g2 - g1) * localT),
76
+ Math.round(b1 + (b2 - b1) * localT),
77
+ ];
78
+ }
79
+
80
+ function rainbowColor(
81
+ freq: number,
82
+ spread: number,
83
+ seed: number,
84
+ x: number,
85
+ y: number,
86
+ ): [number, number, number] {
87
+ const h = ((freq * (x / spread) + seed + y * freq / spread) % 1.0 + 1.0) %
88
+ 1.0;
89
+ return hslToRgb(h, 1.0, 0.5);
90
+ }
91
+
10
92
  export default class DefaultAsciiBannerGeneratorService
11
93
  implements AsciiBannerGeneratorService {
12
94
  #registeredFontNames: Array<string> = [];
@@ -28,22 +110,177 @@ export default class DefaultAsciiBannerGeneratorService
28
110
  figlet.parseFont(fontName, fontDefinition);
29
111
  }
30
112
 
31
- async generate(message: string, fontName: string): Promise<string> {
32
- const name = fontName.toLowerCase();
33
- if (!this.getRegisteredFonts().includes(name)) {
34
- throw new Error(`Font name is not registered: ${name}`);
113
+ protected applyForegroundEffect(
114
+ lines: string[],
115
+ effect: ColorEffect,
116
+ ): string[] {
117
+ if (effect.type === "fixed") {
118
+ const [r, g, b] = parseHex(effect.color);
119
+ const code = foregroundColorStart(r, g, b);
120
+ return lines.map((line) =>
121
+ line.split("").map((ch) =>
122
+ ch === " " ? ch : code + ch + FOREGROUND_COLOR_END
123
+ ).join(
124
+ "",
125
+ )
126
+ );
127
+ }
128
+ if (effect.type === "gradient") {
129
+ const colorStops = effect.colors.map(parseHex);
130
+ return lines.map((line, yi) => {
131
+ if (effect.direction === "vertical") {
132
+ const [r, g, b] = interpolateColors(
133
+ colorStops,
134
+ yi,
135
+ Math.max(lines.length, 1),
136
+ );
137
+ const code = foregroundColorStart(r, g, b);
138
+ return line.split("").map((ch) =>
139
+ ch === " " ? ch : code + ch + FOREGROUND_COLOR_END
140
+ ).join("");
141
+ }
142
+ const nonSpaceCount = line.split("").filter((c) => c !== " ").length;
143
+ let charIdx = 0;
144
+ return line.split("").map((ch) => {
145
+ if (ch === " ") return ch;
146
+ const [r, g, b] = interpolateColors(
147
+ colorStops,
148
+ charIdx,
149
+ Math.max(nonSpaceCount, 1),
150
+ );
151
+ charIdx++;
152
+ return foregroundColorStart(r, g, b) + ch + FOREGROUND_COLOR_END;
153
+ }).join("");
154
+ });
155
+ }
156
+ const freq = effect.frequency ?? 0.3;
157
+ const spread = effect.spread ?? 8.0;
158
+ const seed = effect.seed ?? Math.random();
159
+ return lines.map((line, yi) => {
160
+ if (effect.direction === "vertical") {
161
+ const [r, g, b] = rainbowColor(freq, spread, seed, 0, yi);
162
+ const code = foregroundColorStart(r, g, b);
163
+ return line.split("").map((ch) =>
164
+ ch === " " ? ch : code + ch + FOREGROUND_COLOR_END
165
+ ).join("");
166
+ }
167
+ let charIdx = 0;
168
+ return line.split("").map((ch) => {
169
+ if (ch === " ") return ch;
170
+ const [r, g, b] = rainbowColor(freq, spread, seed, charIdx, yi);
171
+ charIdx++;
172
+ return foregroundColorStart(r, g, b) + ch + FOREGROUND_COLOR_END;
173
+ }).join("");
174
+ });
175
+ }
176
+
177
+ protected applyBackgroundEffect(
178
+ lines: string[],
179
+ effect: ColorEffect,
180
+ ): string[] {
181
+ if (effect.type === "fixed") {
182
+ const [r, g, b] = parseHex(effect.color);
183
+ const code = backgroundColorStart(r, g, b);
184
+ return lines.map((line) => code + line + BACKGROUND_COLOR_END);
185
+ }
186
+ if (effect.type === "gradient") {
187
+ const colorStops = effect.colors.map(parseHex);
188
+ return lines.map((line, yi) => {
189
+ if (effect.direction === "vertical") {
190
+ const [r, g, b] = interpolateColors(
191
+ colorStops,
192
+ yi,
193
+ Math.max(lines.length, 1),
194
+ );
195
+ return backgroundColorStart(r, g, b) + line + BACKGROUND_COLOR_END;
196
+ }
197
+ const totalChars = Math.max(line.length, 1);
198
+ return line.split("").map((ch, xi) => {
199
+ const [r, g, b] = interpolateColors(colorStops, xi, totalChars);
200
+ return backgroundColorStart(r, g, b) + ch + BACKGROUND_COLOR_END;
201
+ }).join("");
202
+ });
203
+ }
204
+ const freq = effect.frequency ?? 0.3;
205
+ const spread = effect.spread ?? 8.0;
206
+ const seed = effect.seed ?? Math.random();
207
+ return lines.map((line, yi) => {
208
+ if (effect.direction === "vertical") {
209
+ const [r, g, b] = rainbowColor(freq, spread, seed, 0, yi);
210
+ return backgroundColorStart(r, g, b) + line + BACKGROUND_COLOR_END;
211
+ }
212
+ return line.split("").map((ch, xi) => {
213
+ const [r, g, b] = rainbowColor(freq, spread, seed, xi, yi);
214
+ return backgroundColorStart(r, g, b) + ch + BACKGROUND_COLOR_END;
215
+ }).join("");
216
+ });
217
+ }
218
+
219
+ async generate(
220
+ message: string,
221
+ options?: BannerGenerateOptions,
222
+ ): Promise<string> {
223
+ const fontName = (options?.fontName ?? "standard").toLowerCase();
224
+ if (!this.getRegisteredFonts().includes(fontName)) {
225
+ throw new Error(`Font name is not registered: ${fontName}`);
226
+ }
227
+
228
+ const bannerText = await figlet.text(message, { font: fontName });
229
+ let titleLines = bannerText.split("\n");
230
+
231
+ while (titleLines.length > 0) {
232
+ const last = titleLines[titleLines.length - 1];
233
+ if (last === undefined || last.trim().length > 0) break;
234
+ titleLines.pop();
235
+ }
236
+
237
+ let subMessageLine: string | undefined;
238
+ if (options?.subMessage) {
239
+ subMessageLine = options.subMessage
240
+ .toUpperCase()
241
+ .split("")
242
+ .map((c) => c === " " ? " " : c + " ")
243
+ .join("")
244
+ .trimEnd();
245
+
246
+ const bannerWidth = Math.max(...titleLines.map((l) => l.length));
247
+ const subWidth = subMessageLine.length;
248
+
249
+ if (bannerWidth >= subWidth) {
250
+ const pad = Math.floor((bannerWidth - subWidth) / 2);
251
+ subMessageLine = " ".repeat(pad) + subMessageLine;
252
+ } else {
253
+ const pad = Math.floor((subWidth - bannerWidth) / 2);
254
+ titleLines = titleLines.map((l) => " ".repeat(pad) + l);
255
+ }
256
+ }
257
+
258
+ const colorEffects = options?.colorEffects;
259
+
260
+ if (colorEffects?.messageForeground) {
261
+ titleLines = this.applyForegroundEffect(
262
+ titleLines,
263
+ colorEffects.messageForeground,
264
+ );
265
+ }
266
+
267
+ let subLines: string[] | undefined;
268
+ if (subMessageLine !== undefined) {
269
+ subLines = [subMessageLine];
270
+ if (colorEffects?.subMessageForeground) {
271
+ subLines = this.applyForegroundEffect(
272
+ subLines,
273
+ colorEffects.subMessageForeground,
274
+ );
275
+ }
35
276
  }
36
277
 
37
- const bannerText = await figlet.text(message, { font: name });
38
- const lines = bannerText.split("\n");
278
+ let allLines = subLines ? [...titleLines, ...subLines] : titleLines;
39
279
 
40
- // Remove any whitespace lines from the end
41
- while (
42
- (lines.length > 0) && (lines[lines.length - 1].trim().length === 0)
43
- ) {
44
- lines.pop();
280
+ if (colorEffects?.background) {
281
+ allLines = this.applyBackgroundEffect(allLines, colorEffects.background);
45
282
  }
46
283
 
47
- return "\n" + lines.join("\n") + "\n";
284
+ return "\n" + allLines.join("\n") + "\n";
48
285
  }
49
286
  }
@@ -46,7 +46,7 @@ export default class BannerServiceProvider implements ServiceProvider {
46
46
  this.#configurationServiceProvider = configurationServiceProvider;
47
47
  }
48
48
 
49
- public provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
49
+ public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
50
50
  return Promise.resolve({
51
51
  commands: [
52
52
  new NoBannerCommand(this, this.servicePriority),
@@ -70,7 +70,7 @@ export default class BannerServiceProvider implements ServiceProvider {
70
70
  const { cliConfig } = context;
71
71
  const bannerText = await asciiBannerGeneratorService.generate(
72
72
  cliConfig.name.toUpperCase(),
73
- this.fontName,
73
+ { fontName: this.fontName, subMessage: cliConfig.subMessage },
74
74
  );
75
75
 
76
76
  await printerService.info(printerService.blue(bannerText));