@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,754 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import DefaultArgumentPrompterService from "../../../src/service/argumentPrompter/DefaultArgumentPrompterService.ts";
|
|
3
|
+
import type PrompterService from "../../../src/api/service/core/PrompterService.ts";
|
|
4
|
+
import type {
|
|
5
|
+
Prompt,
|
|
6
|
+
PromptResult,
|
|
7
|
+
} from "../../../src/api/service/core/PrompterService.ts";
|
|
8
|
+
import type { ParseResult } from "../../../src/runtime/parser.ts";
|
|
9
|
+
import { InvalidArgumentReason } from "../../../src/api/RunResult.ts";
|
|
10
|
+
import {
|
|
11
|
+
ArgumentValueTypeName,
|
|
12
|
+
ComplexValueTypeName,
|
|
13
|
+
} from "../../../src/api/argument/ArgumentValueTypes.ts";
|
|
14
|
+
import type SubCommand from "../../../src/api/command/SubCommand.ts";
|
|
15
|
+
import type GlobalCommand from "../../../src/api/command/GlobalCommand.ts";
|
|
16
|
+
import type ComplexOption from "../../../src/api/argument/ComplexOption.ts";
|
|
17
|
+
|
|
18
|
+
class MockPrompterService implements PrompterService {
|
|
19
|
+
promptEnabled = true;
|
|
20
|
+
#responses: Map<string, PromptResult> = new Map();
|
|
21
|
+
|
|
22
|
+
addResponse(name: string, value: PromptResult["value"]) {
|
|
23
|
+
this.#responses.set(name, { name, value });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
prompt(promptDef: Prompt): Promise<PromptResult> {
|
|
27
|
+
const result = this.#responses.get(promptDef.name);
|
|
28
|
+
if (!result) {
|
|
29
|
+
return Promise.reject(
|
|
30
|
+
new Error(`No mock response for prompt: ${promptDef.name}`),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve(result);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async promptAll(
|
|
37
|
+
prompts: ReadonlyArray<Prompt>,
|
|
38
|
+
): Promise<ReadonlyArray<PromptResult>> {
|
|
39
|
+
const results: PromptResult[] = [];
|
|
40
|
+
for (const p of prompts) {
|
|
41
|
+
results.push(await this.prompt(p));
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeSubCommand(
|
|
48
|
+
name: string,
|
|
49
|
+
options: SubCommand["options"] = [],
|
|
50
|
+
positionals: SubCommand["positionals"] = [],
|
|
51
|
+
): SubCommand {
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
description: `${name} description`,
|
|
55
|
+
options,
|
|
56
|
+
positionals,
|
|
57
|
+
enableConfiguration: false,
|
|
58
|
+
execute: () => Promise.resolve(),
|
|
59
|
+
} as SubCommand;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeGlobalCommand(
|
|
63
|
+
name: string,
|
|
64
|
+
argType: ArgumentValueTypeName = ArgumentValueTypeName.STRING,
|
|
65
|
+
): GlobalCommand {
|
|
66
|
+
return {
|
|
67
|
+
name,
|
|
68
|
+
description: `${name} description`,
|
|
69
|
+
argument: {
|
|
70
|
+
type: argType,
|
|
71
|
+
},
|
|
72
|
+
} as GlobalCommand;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("DefaultArgumentPrompterService tests", () => {
|
|
76
|
+
test("promptEnabled=false returns original parseResult", async () => {
|
|
77
|
+
const prompter = new MockPrompterService();
|
|
78
|
+
prompter.promptEnabled = false;
|
|
79
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
80
|
+
|
|
81
|
+
const command = makeSubCommand("test", [
|
|
82
|
+
{
|
|
83
|
+
name: "foo",
|
|
84
|
+
description: "foo option",
|
|
85
|
+
type: ArgumentValueTypeName.STRING,
|
|
86
|
+
isOptional: false,
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const parseResult: ParseResult = {
|
|
91
|
+
command,
|
|
92
|
+
populatedArgumentValues: {},
|
|
93
|
+
invalidArguments: [
|
|
94
|
+
{
|
|
95
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
96
|
+
name: "foo",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
unusedArgs: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
103
|
+
expect(result).toBe(parseResult);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("non-MISSING_VALUE invalidArguments returns original parseResult", async () => {
|
|
107
|
+
const prompter = new MockPrompterService();
|
|
108
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
109
|
+
|
|
110
|
+
const command = makeSubCommand("test");
|
|
111
|
+
const parseResult: ParseResult = {
|
|
112
|
+
command,
|
|
113
|
+
populatedArgumentValues: {},
|
|
114
|
+
invalidArguments: [
|
|
115
|
+
{
|
|
116
|
+
reason: InvalidArgumentReason.INCORRECT_VALUE_TYPE,
|
|
117
|
+
name: "foo",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
unusedArgs: [],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
124
|
+
expect(result).toBe(parseResult);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("empty invalidArguments returns original parseResult", async () => {
|
|
128
|
+
const prompter = new MockPrompterService();
|
|
129
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
130
|
+
|
|
131
|
+
const command = makeSubCommand("test");
|
|
132
|
+
const parseResult: ParseResult = {
|
|
133
|
+
command,
|
|
134
|
+
populatedArgumentValues: {},
|
|
135
|
+
invalidArguments: [],
|
|
136
|
+
unusedArgs: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
140
|
+
expect(result).toBe(parseResult);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("missing STRING option prompts TEXT", async () => {
|
|
144
|
+
const prompter = new MockPrompterService();
|
|
145
|
+
prompter.addResponse("foo", "bar");
|
|
146
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
147
|
+
|
|
148
|
+
const command = makeSubCommand("test", [
|
|
149
|
+
{
|
|
150
|
+
name: "foo",
|
|
151
|
+
description: "foo option",
|
|
152
|
+
type: ArgumentValueTypeName.STRING,
|
|
153
|
+
isOptional: false,
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const parseResult: ParseResult = {
|
|
158
|
+
command,
|
|
159
|
+
populatedArgumentValues: {},
|
|
160
|
+
invalidArguments: [
|
|
161
|
+
{
|
|
162
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
163
|
+
name: "foo",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
unusedArgs: [],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
170
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
171
|
+
expect(
|
|
172
|
+
(result.populatedArgumentValues as Record<string, unknown>)["foo"],
|
|
173
|
+
).toEqual("bar");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("missing BOOLEAN option prompts TOGGLE", async () => {
|
|
177
|
+
const prompter = new MockPrompterService();
|
|
178
|
+
prompter.addResponse("flag", true);
|
|
179
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
180
|
+
|
|
181
|
+
const command = makeSubCommand("test", [
|
|
182
|
+
{
|
|
183
|
+
name: "flag",
|
|
184
|
+
description: "flag option",
|
|
185
|
+
type: ArgumentValueTypeName.BOOLEAN,
|
|
186
|
+
isOptional: false,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const parseResult: ParseResult = {
|
|
191
|
+
command,
|
|
192
|
+
populatedArgumentValues: {},
|
|
193
|
+
invalidArguments: [
|
|
194
|
+
{
|
|
195
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
196
|
+
name: "flag",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
unusedArgs: [],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
203
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
204
|
+
expect(
|
|
205
|
+
(result.populatedArgumentValues as Record<string, unknown>)["flag"],
|
|
206
|
+
).toEqual(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("missing option with allowableValues prompts SINGLE_SELECT", async () => {
|
|
210
|
+
const prompter = new MockPrompterService();
|
|
211
|
+
prompter.addResponse("color", "blue");
|
|
212
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
213
|
+
|
|
214
|
+
const command = makeSubCommand("test", [
|
|
215
|
+
{
|
|
216
|
+
name: "color",
|
|
217
|
+
description: "color option",
|
|
218
|
+
type: ArgumentValueTypeName.STRING,
|
|
219
|
+
isOptional: false,
|
|
220
|
+
allowableValues: ["red", "blue", "green"],
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
const parseResult: ParseResult = {
|
|
225
|
+
command,
|
|
226
|
+
populatedArgumentValues: {},
|
|
227
|
+
invalidArguments: [
|
|
228
|
+
{
|
|
229
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
230
|
+
name: "color",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
unusedArgs: [],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
237
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
238
|
+
expect(
|
|
239
|
+
(result.populatedArgumentValues as Record<string, unknown>)["color"],
|
|
240
|
+
).toEqual("blue");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("missing INTEGER option coerces string to number", async () => {
|
|
244
|
+
const prompter = new MockPrompterService();
|
|
245
|
+
prompter.addResponse("count", "42");
|
|
246
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
247
|
+
|
|
248
|
+
const command = makeSubCommand("test", [
|
|
249
|
+
{
|
|
250
|
+
name: "count",
|
|
251
|
+
description: "count option",
|
|
252
|
+
type: ArgumentValueTypeName.INTEGER,
|
|
253
|
+
isOptional: false,
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const parseResult: ParseResult = {
|
|
258
|
+
command,
|
|
259
|
+
populatedArgumentValues: {},
|
|
260
|
+
invalidArguments: [
|
|
261
|
+
{
|
|
262
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
263
|
+
name: "count",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
unusedArgs: [],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
270
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
271
|
+
expect(
|
|
272
|
+
(result.populatedArgumentValues as Record<string, unknown>)["count"],
|
|
273
|
+
).toEqual(42);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("SubCommand with multiple missing values prompts for each", async () => {
|
|
277
|
+
const prompter = new MockPrompterService();
|
|
278
|
+
prompter.addResponse("name", "Alice");
|
|
279
|
+
prompter.addResponse("age", "30");
|
|
280
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
281
|
+
|
|
282
|
+
const command = makeSubCommand("test", [
|
|
283
|
+
{
|
|
284
|
+
name: "name",
|
|
285
|
+
description: "name option",
|
|
286
|
+
type: ArgumentValueTypeName.STRING,
|
|
287
|
+
isOptional: false,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "age",
|
|
291
|
+
description: "age option",
|
|
292
|
+
type: ArgumentValueTypeName.INTEGER,
|
|
293
|
+
isOptional: false,
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
const parseResult: ParseResult = {
|
|
298
|
+
command,
|
|
299
|
+
populatedArgumentValues: {},
|
|
300
|
+
invalidArguments: [
|
|
301
|
+
{
|
|
302
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
303
|
+
name: "name",
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
307
|
+
name: "age",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
unusedArgs: [],
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
314
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
315
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
316
|
+
expect(values["name"]).toEqual("Alice");
|
|
317
|
+
expect(values["age"]).toEqual(30);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("GlobalCommand with missing argument prompts and returns value", async () => {
|
|
321
|
+
const prompter = new MockPrompterService();
|
|
322
|
+
prompter.addResponse("myglob", "hello");
|
|
323
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
324
|
+
|
|
325
|
+
const command = makeGlobalCommand("myglob");
|
|
326
|
+
|
|
327
|
+
const parseResult: ParseResult = {
|
|
328
|
+
command,
|
|
329
|
+
populatedArgumentValues: undefined,
|
|
330
|
+
invalidArguments: [
|
|
331
|
+
{
|
|
332
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
333
|
+
name: "myglob",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
unusedArgs: [],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
340
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
341
|
+
expect(result.populatedArgumentValues).toEqual("hello");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("GlobalCommand with no argument returns original parseResult", async () => {
|
|
345
|
+
const prompter = new MockPrompterService();
|
|
346
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
347
|
+
|
|
348
|
+
// GlobalCommand with no argument property
|
|
349
|
+
const command = {
|
|
350
|
+
name: "noarg",
|
|
351
|
+
description: "no arg command",
|
|
352
|
+
} as GlobalCommand;
|
|
353
|
+
|
|
354
|
+
const parseResult: ParseResult = {
|
|
355
|
+
command,
|
|
356
|
+
populatedArgumentValues: undefined,
|
|
357
|
+
invalidArguments: [
|
|
358
|
+
{
|
|
359
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
360
|
+
name: "noarg",
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
unusedArgs: [],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
367
|
+
expect(result).toBe(parseResult);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("GlobalCommand with INTEGER argument coerces value", async () => {
|
|
371
|
+
const prompter = new MockPrompterService();
|
|
372
|
+
prompter.addResponse("count", "99");
|
|
373
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
374
|
+
|
|
375
|
+
const command = makeGlobalCommand("count", ArgumentValueTypeName.INTEGER);
|
|
376
|
+
|
|
377
|
+
const parseResult: ParseResult = {
|
|
378
|
+
command,
|
|
379
|
+
populatedArgumentValues: undefined,
|
|
380
|
+
invalidArguments: [
|
|
381
|
+
{
|
|
382
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
383
|
+
name: "count",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
unusedArgs: [],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
390
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
391
|
+
expect(result.populatedArgumentValues).toEqual(99);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("SubCommand with complex option prompts for nested properties", async () => {
|
|
395
|
+
const prompter = new MockPrompterService();
|
|
396
|
+
prompter.addResponse("addr.street", "123 Main St");
|
|
397
|
+
prompter.addResponse("addr.city", "Springfield");
|
|
398
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
399
|
+
|
|
400
|
+
const complexOption: ComplexOption = {
|
|
401
|
+
name: "addr",
|
|
402
|
+
description: "address",
|
|
403
|
+
type: ComplexValueTypeName.COMPLEX,
|
|
404
|
+
properties: [
|
|
405
|
+
{
|
|
406
|
+
name: "street",
|
|
407
|
+
description: "street name",
|
|
408
|
+
type: ArgumentValueTypeName.STRING,
|
|
409
|
+
isOptional: false,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "city",
|
|
413
|
+
description: "city name",
|
|
414
|
+
type: ArgumentValueTypeName.STRING,
|
|
415
|
+
isOptional: false,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const command = makeSubCommand(
|
|
421
|
+
"test",
|
|
422
|
+
[complexOption as unknown as SubCommand["options"][number]],
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const parseResult: ParseResult = {
|
|
426
|
+
command,
|
|
427
|
+
populatedArgumentValues: {},
|
|
428
|
+
invalidArguments: [
|
|
429
|
+
{
|
|
430
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
431
|
+
name: "addr",
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
unusedArgs: [],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
438
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
439
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
440
|
+
const addr = values["addr"] as Record<string, unknown>;
|
|
441
|
+
expect(addr["street"]).toEqual("123 Main St");
|
|
442
|
+
expect(addr["city"]).toEqual("Springfield");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("SubCommand with complex option skips optional properties with defaults", async () => {
|
|
446
|
+
const prompter = new MockPrompterService();
|
|
447
|
+
prompter.addResponse("addr.street", "123 Main St");
|
|
448
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
449
|
+
|
|
450
|
+
const complexOption: ComplexOption = {
|
|
451
|
+
name: "addr",
|
|
452
|
+
description: "address",
|
|
453
|
+
type: ComplexValueTypeName.COMPLEX,
|
|
454
|
+
properties: [
|
|
455
|
+
{
|
|
456
|
+
name: "street",
|
|
457
|
+
description: "street name",
|
|
458
|
+
type: ArgumentValueTypeName.STRING,
|
|
459
|
+
isOptional: false,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "zip",
|
|
463
|
+
description: "zip code",
|
|
464
|
+
type: ArgumentValueTypeName.STRING,
|
|
465
|
+
isOptional: true,
|
|
466
|
+
defaultValue: "00000",
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const command = makeSubCommand(
|
|
472
|
+
"test",
|
|
473
|
+
[complexOption as unknown as SubCommand["options"][number]],
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const parseResult: ParseResult = {
|
|
477
|
+
command,
|
|
478
|
+
populatedArgumentValues: {},
|
|
479
|
+
invalidArguments: [
|
|
480
|
+
{
|
|
481
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
482
|
+
name: "addr",
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
unusedArgs: [],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
489
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
490
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
491
|
+
const addr = values["addr"] as Record<string, unknown>;
|
|
492
|
+
expect(addr["street"]).toEqual("123 Main St");
|
|
493
|
+
expect(addr["zip"]).toBeUndefined();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("SubCommand with array option prompts for multiple values", async () => {
|
|
497
|
+
const prompter = new MockPrompterService();
|
|
498
|
+
prompter.addResponse("tags", "alpha");
|
|
499
|
+
prompter.addResponse("tags_more", true);
|
|
500
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
501
|
+
|
|
502
|
+
// Override prompt to return different values on successive calls
|
|
503
|
+
let callCount = 0;
|
|
504
|
+
const responses: Array<{ name: string; value: unknown }> = [
|
|
505
|
+
{ name: "tags", value: "alpha" },
|
|
506
|
+
{ name: "tags_more", value: true },
|
|
507
|
+
{ name: "tags", value: "beta" },
|
|
508
|
+
{ name: "tags_more", value: false },
|
|
509
|
+
];
|
|
510
|
+
prompter.prompt = () => {
|
|
511
|
+
const resp = responses[callCount]!;
|
|
512
|
+
callCount++;
|
|
513
|
+
return Promise.resolve(resp as PromptResult);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const command = makeSubCommand("test", [
|
|
517
|
+
{
|
|
518
|
+
name: "tags",
|
|
519
|
+
description: "tags list",
|
|
520
|
+
type: ArgumentValueTypeName.STRING,
|
|
521
|
+
isOptional: false,
|
|
522
|
+
isArray: true,
|
|
523
|
+
},
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
const parseResult: ParseResult = {
|
|
527
|
+
command,
|
|
528
|
+
populatedArgumentValues: {},
|
|
529
|
+
invalidArguments: [
|
|
530
|
+
{
|
|
531
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
532
|
+
name: "tags",
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
unusedArgs: [],
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
539
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
540
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
541
|
+
expect(values["tags"]).toEqual(["alpha", "beta"]);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("SubCommand with missing positional prompts for value", async () => {
|
|
545
|
+
const prompter = new MockPrompterService();
|
|
546
|
+
prompter.addResponse("file", "/tmp/data.txt");
|
|
547
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
548
|
+
|
|
549
|
+
const command = makeSubCommand("test", [], [
|
|
550
|
+
{
|
|
551
|
+
name: "file",
|
|
552
|
+
description: "file path",
|
|
553
|
+
type: ArgumentValueTypeName.STRING,
|
|
554
|
+
},
|
|
555
|
+
]);
|
|
556
|
+
|
|
557
|
+
const parseResult: ParseResult = {
|
|
558
|
+
command,
|
|
559
|
+
populatedArgumentValues: {},
|
|
560
|
+
invalidArguments: [
|
|
561
|
+
{
|
|
562
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
563
|
+
name: "file",
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
unusedArgs: [],
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
570
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
571
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
572
|
+
expect(values["file"]).toEqual("/tmp/data.txt");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("SubCommand with unknown argument name pushes to remainingInvalid", async () => {
|
|
576
|
+
const prompter = new MockPrompterService();
|
|
577
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
578
|
+
|
|
579
|
+
const command = makeSubCommand("test", [
|
|
580
|
+
{
|
|
581
|
+
name: "known",
|
|
582
|
+
description: "known option",
|
|
583
|
+
type: ArgumentValueTypeName.STRING,
|
|
584
|
+
isOptional: false,
|
|
585
|
+
},
|
|
586
|
+
]);
|
|
587
|
+
|
|
588
|
+
const parseResult: ParseResult = {
|
|
589
|
+
command,
|
|
590
|
+
populatedArgumentValues: {},
|
|
591
|
+
invalidArguments: [
|
|
592
|
+
{
|
|
593
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
594
|
+
name: "unknown_arg",
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
unusedArgs: [],
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
601
|
+
expect(result.invalidArguments.length).toEqual(1);
|
|
602
|
+
expect(result.invalidArguments[0]!.name).toEqual("unknown_arg");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("SubCommand with invalid name=undefined pushes to remainingInvalid", async () => {
|
|
606
|
+
const prompter = new MockPrompterService();
|
|
607
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
608
|
+
|
|
609
|
+
const command = makeSubCommand("test");
|
|
610
|
+
|
|
611
|
+
const parseResult: ParseResult = {
|
|
612
|
+
command,
|
|
613
|
+
populatedArgumentValues: {},
|
|
614
|
+
invalidArguments: [
|
|
615
|
+
{
|
|
616
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
unusedArgs: [],
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
623
|
+
expect(result.invalidArguments.length).toEqual(1);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("NUMBER type coerces string to float", async () => {
|
|
627
|
+
const prompter = new MockPrompterService();
|
|
628
|
+
prompter.addResponse("ratio", "3.14");
|
|
629
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
630
|
+
|
|
631
|
+
const command = makeSubCommand("test", [
|
|
632
|
+
{
|
|
633
|
+
name: "ratio",
|
|
634
|
+
description: "ratio option",
|
|
635
|
+
type: ArgumentValueTypeName.NUMBER,
|
|
636
|
+
isOptional: false,
|
|
637
|
+
},
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
const parseResult: ParseResult = {
|
|
641
|
+
command,
|
|
642
|
+
populatedArgumentValues: {},
|
|
643
|
+
invalidArguments: [
|
|
644
|
+
{
|
|
645
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
646
|
+
name: "ratio",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
unusedArgs: [],
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
653
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
654
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
655
|
+
expect(values["ratio"]).toBeCloseTo(3.14);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("BOOLEAN type coerces string 'true' to boolean true", async () => {
|
|
659
|
+
const prompter = new MockPrompterService();
|
|
660
|
+
prompter.addResponse("enabled", "true");
|
|
661
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
662
|
+
|
|
663
|
+
const command = makeSubCommand("test", [
|
|
664
|
+
{
|
|
665
|
+
name: "enabled",
|
|
666
|
+
description: "enabled option",
|
|
667
|
+
type: ArgumentValueTypeName.BOOLEAN,
|
|
668
|
+
isOptional: false,
|
|
669
|
+
},
|
|
670
|
+
]);
|
|
671
|
+
|
|
672
|
+
const parseResult: ParseResult = {
|
|
673
|
+
command,
|
|
674
|
+
populatedArgumentValues: {},
|
|
675
|
+
invalidArguments: [
|
|
676
|
+
{
|
|
677
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
678
|
+
name: "enabled",
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
unusedArgs: [],
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
685
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
686
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
687
|
+
expect(values["enabled"]).toEqual(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("prompt error returns original parseResult", async () => {
|
|
691
|
+
const prompter = new MockPrompterService();
|
|
692
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
693
|
+
|
|
694
|
+
// No mock responses added, so prompt will reject
|
|
695
|
+
const command = makeGlobalCommand("failing");
|
|
696
|
+
|
|
697
|
+
const parseResult: ParseResult = {
|
|
698
|
+
command,
|
|
699
|
+
populatedArgumentValues: undefined,
|
|
700
|
+
invalidArguments: [
|
|
701
|
+
{
|
|
702
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
703
|
+
name: "failing",
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
unusedArgs: [],
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
710
|
+
expect(result).toBe(parseResult);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("vararg positional triggers array prompting", async () => {
|
|
714
|
+
const prompter = new MockPrompterService();
|
|
715
|
+
const service = new DefaultArgumentPrompterService(prompter);
|
|
716
|
+
|
|
717
|
+
let callCount = 0;
|
|
718
|
+
const responses: Array<{ name: string; value: unknown }> = [
|
|
719
|
+
{ name: "files", value: "a.txt" },
|
|
720
|
+
{ name: "files_more", value: false },
|
|
721
|
+
];
|
|
722
|
+
prompter.prompt = () => {
|
|
723
|
+
const resp = responses[callCount]!;
|
|
724
|
+
callCount++;
|
|
725
|
+
return Promise.resolve(resp as PromptResult);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const command = makeSubCommand("test", [], [
|
|
729
|
+
{
|
|
730
|
+
name: "files",
|
|
731
|
+
description: "file paths",
|
|
732
|
+
type: ArgumentValueTypeName.STRING,
|
|
733
|
+
isVarargMultiple: true,
|
|
734
|
+
},
|
|
735
|
+
]);
|
|
736
|
+
|
|
737
|
+
const parseResult: ParseResult = {
|
|
738
|
+
command,
|
|
739
|
+
populatedArgumentValues: {},
|
|
740
|
+
invalidArguments: [
|
|
741
|
+
{
|
|
742
|
+
reason: InvalidArgumentReason.MISSING_VALUE,
|
|
743
|
+
name: "files",
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
unusedArgs: [],
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const result = await service.promptForMissingArguments(parseResult);
|
|
750
|
+
expect(result.invalidArguments.length).toEqual(0);
|
|
751
|
+
const values = result.populatedArgumentValues as Record<string, unknown>;
|
|
752
|
+
expect(values["files"]).toEqual(["a.txt"]);
|
|
753
|
+
});
|
|
754
|
+
});
|