@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.
- 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 +97 -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 +24 -0
- 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 +18 -2
- package/src/command/SingleCommandCliHelpCommand.ts +10 -1
- package/src/launcher.ts +16 -58
- package/src/runtime/command/CommandValidator.ts +3 -3
- package/src/runtime/runner.ts +102 -30
- package/src/runtime/scanner.ts +18 -15
- package/src/runtime/values/argumentValueValidation.ts +49 -5
- 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/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 +66 -122
- package/src/util/logger.ts +16 -14
- package/src/util/runnerHelper.ts +1 -1
- 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/parser.test.ts +9 -7
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
- package/tests/runtime/values/argumentValueValidation.test.ts +193 -0
- 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 +7 -5
- 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
|
@@ -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: [],
|
|
@@ -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
|
-
|
|
8
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
const lines = bannerText.split("\n");
|
|
278
|
+
let allLines = subLines ? [...titleLines, ...subLines] : titleLines;
|
|
39
279
|
|
|
40
|
-
|
|
41
|
-
|
|
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" +
|
|
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
|
|
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));
|