@flowscripter/dynamic-cli-framework 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release-bun-library.yml +1 -0
- package/README/core-commands.md +40 -0
- package/README/core-service-providers.md +198 -0
- package/README/development.md +60 -0
- package/README/implementation-details.md +474 -0
- package/README/key-concepts.md +751 -0
- package/README.md +12 -1413
- package/bun.lock +20 -18
- package/index.ts +98 -1
- package/package.json +8 -7
- package/src/api/BaseCLIFeatureOptions.ts +10 -0
- package/src/api/CLIConfig.ts +5 -0
- package/src/api/RunResult.ts +16 -0
- package/src/api/argument/Argument.ts +40 -1
- package/src/api/argument/ArgumentValueTypes.ts +1 -1
- package/src/api/command/SubCommand.ts +1 -1
- package/src/api/service/ServiceProvider.ts +3 -3
- package/src/api/service/core/ArgumentPrompterService.ts +10 -0
- package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
- package/src/api/service/core/CompletionService.ts +36 -0
- package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
- package/src/api/service/core/ImagePrinterService.ts +26 -0
- package/src/api/service/core/KeyValueService.ts +22 -5
- package/src/api/service/core/PrinterService.ts +127 -1
- package/src/api/service/core/PrompterService.ts +49 -0
- package/src/api/service/core/SecretService.ts +27 -0
- package/src/api/service/core/ShutdownService.ts +17 -1
- package/src/api/service/core/Table.ts +85 -0
- package/src/api/service/core/TableGeneratorService.ts +38 -0
- package/src/api/service/core/TreePrinterService.ts +11 -0
- package/src/cli/BaseCLI.ts +116 -44
- package/src/cli/DefaultRuntimeCLI.ts +10 -18
- package/src/command/CompletionCommand.ts +179 -0
- package/src/command/MultiCommandCliHelpCommand.ts +20 -32
- package/src/command/SingleCommandCliHelpCommand.ts +10 -1
- package/src/launcher.ts +16 -58
- package/src/runtime/command/CommandValidator.ts +70 -6
- package/src/runtime/runner.ts +112 -31
- package/src/runtime/scanner.ts +18 -15
- package/src/runtime/values/argumentValueValidation.ts +93 -10
- package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
- package/src/runtime/values/subCommandValuePopulation.ts +9 -8
- package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
- package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
- package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
- package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
- package/src/service/banner/BannerServiceProvider.ts +2 -2
- package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
- package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
- package/src/service/completion/CompletionServiceProvider.ts +212 -0
- package/src/service/completion/DefaultCompletionService.ts +364 -0
- package/src/service/completion/shell/ShellHandler.ts +16 -0
- package/src/service/completion/shell/bash.ts +47 -0
- package/src/service/completion/shell/fish.ts +43 -0
- package/src/service/completion/shell/powershell.ts +66 -0
- package/src/service/completion/shell/zsh.ts +50 -0
- package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
- package/src/service/configuration/DefaultKeyValueService.ts +53 -7
- package/src/service/configuration/DefaultSecretService.ts +85 -0
- package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
- package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
- package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
- package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
- package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
- package/src/service/printer/DefaultPrinterService.ts +147 -27
- package/src/service/printer/PrinterServiceProvider.ts +7 -7
- package/src/service/printer/command/LogLevelCommand.ts +2 -1
- package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
- package/src/service/printer/terminal/Progress.ts +21 -4
- package/src/service/printer/terminal/Spinner.ts +32 -8
- package/src/service/printer/terminal/Theme.ts +1 -1
- package/src/service/prompter/DefaultPrompterService.ts +87 -0
- package/src/service/prompter/PrompterServiceProvider.ts +36 -0
- package/src/service/prompter/command/NoPromptCommand.ts +40 -0
- package/src/service/prompter/prompt/PromptContext.ts +47 -0
- package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
- package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
- package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
- package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
- package/src/service/prompter/prompt/promptText.ts +107 -0
- package/src/service/prompter/prompt/promptToggle.ts +28 -0
- package/src/service/shutdown/DefaultShutdownService.ts +13 -0
- package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
- package/src/service/shutdown/ShutdownState.ts +5 -0
- package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
- package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
- package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
- package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
- package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
- package/src/terminal/Ansi.ts +27 -0
- package/src/terminal/ImageRenderer.ts +199 -0
- package/src/terminal/KeyReader.ts +23 -0
- package/src/terminal/PngDecoder.ts +143 -0
- package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
- package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
- package/src/terminal/TtyKeyReader.ts +83 -0
- package/src/terminal/TtyStyler.ts +78 -0
- package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
- package/src/terminal/supportsHyperlinks.ts +110 -0
- package/src/util/configHelper.ts +2 -2
- package/src/util/envVarHelper.ts +2 -2
- package/src/util/helpHelper.ts +139 -123
- package/src/util/logger.ts +16 -14
- package/src/util/runnerHelper.ts +41 -0
- package/tests/cli/BaseCLI.test.ts +50 -54
- package/tests/command/CompletionCommand.test.ts +68 -0
- package/tests/command/UsageCommand.test.ts +3 -2
- package/tests/command/VersionCommand.test.ts +3 -2
- package/tests/fixtures/Context.ts +9 -2
- package/tests/fixtures/util.ts +1 -1
- package/tests/runtime/command/CommandValidator.test.ts +103 -0
- package/tests/runtime/parser.test.ts +9 -7
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
- package/tests/runtime/runner.test.ts +36 -0
- package/tests/runtime/values/argumentValueValidation.test.ts +215 -1
- package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
- package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
- package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
- package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
- package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
- package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
- package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
- package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
- package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
- package/tests/service/completion/shell/bash.test.ts +73 -0
- package/tests/service/completion/shell/fish.test.ts +62 -0
- package/tests/service/completion/shell/powershell.test.ts +59 -0
- package/tests/service/completion/shell/zsh.test.ts +60 -0
- package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
- package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
- package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
- package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
- package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
- package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
- package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
- package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
- package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
- package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
- package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
- package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
- package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
- package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
- package/tests/service/printer/command/LogLevelCommand.test.ts +8 -6
- package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
- package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
- package/tests/service/printer/terminal/Progress.test.ts +64 -4
- package/tests/service/printer/terminal/Spinner.test.ts +44 -3
- package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
- package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
- package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
- package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
- package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
- package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
- package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
- package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
- package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
- package/tests/terminal/ImageRenderer.test.ts +161 -0
- package/tests/terminal/PngDecoder.test.ts +223 -0
- package/tests/terminal/supportsHyperlinks.test.ts +149 -0
- package/tests/util/configHelper.test.ts +4 -4
- package/tests/util/logger.test.ts +23 -1
- package/tsconfig.json +31 -0
- package/src/service/printer/terminal/Ansi.ts +0 -10
- package/src/service/printer/terminal/TtyStyler.ts +0 -39
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import DefaultPrompterService, {
|
|
3
|
+
DEFAULT_PROMPTER_CONFIG,
|
|
4
|
+
} from "../../../src/service/prompter/DefaultPrompterService.ts";
|
|
5
|
+
import type KeyReader from "../../../src/terminal/KeyReader.ts";
|
|
6
|
+
import { type KeyEvent, SpecialKey } from "../../../src/terminal/KeyReader.ts";
|
|
7
|
+
import type Terminal from "../../../src/terminal/Terminal.ts";
|
|
8
|
+
import { PromptType } from "../../../src/api/service/core/PrompterService.ts";
|
|
9
|
+
import type PrinterService from "../../../src/api/service/core/PrinterService.ts";
|
|
10
|
+
|
|
11
|
+
class MockKeyReader implements KeyReader {
|
|
12
|
+
#keys: KeyEvent[] = [];
|
|
13
|
+
|
|
14
|
+
enableRawMode() {}
|
|
15
|
+
disableRawMode() {}
|
|
16
|
+
|
|
17
|
+
addKeys(...keys: KeyEvent[]) {
|
|
18
|
+
this.#keys.push(...keys);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
readKey(): Promise<KeyEvent> {
|
|
22
|
+
const key = this.#keys.shift();
|
|
23
|
+
if (!key) throw new Error("No more keys");
|
|
24
|
+
return Promise.resolve(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class MockTerminal implements Terminal {
|
|
29
|
+
buffer = "";
|
|
30
|
+
clearUpLinesCalls: number[] = [];
|
|
31
|
+
hideCursorCalled = false;
|
|
32
|
+
showCursorCalled = false;
|
|
33
|
+
|
|
34
|
+
clearLine(): Promise<void> {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clearUpLines(count: number): Promise<void> {
|
|
39
|
+
this.clearUpLinesCalls.push(count);
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hideCursor(): Promise<void> {
|
|
44
|
+
this.hideCursorCalled = true;
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
showCursor(): Promise<void> {
|
|
49
|
+
this.showCursorCalled = true;
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
write(text: string): Promise<void> {
|
|
54
|
+
this.buffer += text;
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
columns(): number {
|
|
59
|
+
return 80;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
rows(): number {
|
|
63
|
+
return 24;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getMockPrinterService(): PrinterService {
|
|
68
|
+
return {
|
|
69
|
+
colorEnabled: false,
|
|
70
|
+
hyperlinksEnabled: false,
|
|
71
|
+
darkMode: false,
|
|
72
|
+
stdoutWritable: new WritableStream(),
|
|
73
|
+
stderrWritable: new WritableStream(),
|
|
74
|
+
primary: (m: string) => m,
|
|
75
|
+
secondary: (m: string) => m,
|
|
76
|
+
emphasised: (m: string) => m,
|
|
77
|
+
selected: (m: string) => m,
|
|
78
|
+
italic: (m: string) => m,
|
|
79
|
+
yellow: (m: string) => m,
|
|
80
|
+
orange: (m: string) => m,
|
|
81
|
+
red: (m: string) => m,
|
|
82
|
+
magenta: (m: string) => m,
|
|
83
|
+
violet: (m: string) => m,
|
|
84
|
+
blue: (m: string) => m,
|
|
85
|
+
cyan: (m: string) => m,
|
|
86
|
+
green: (m: string) => m,
|
|
87
|
+
color: (m: string, _c: string) => m,
|
|
88
|
+
backgroundPrimary: (m: string) => m,
|
|
89
|
+
backgroundSecondary: (m: string) => m,
|
|
90
|
+
backgroundEmphasised: (m: string) => m,
|
|
91
|
+
backgroundSelected: (m: string) => m,
|
|
92
|
+
backgroundYellow: (m: string) => m,
|
|
93
|
+
backgroundOrange: (m: string) => m,
|
|
94
|
+
backgroundRed: (m: string) => m,
|
|
95
|
+
backgroundMagenta: (m: string) => m,
|
|
96
|
+
backgroundViolet: (m: string) => m,
|
|
97
|
+
backgroundBlue: (m: string) => m,
|
|
98
|
+
backgroundCyan: (m: string) => m,
|
|
99
|
+
backgroundGreen: (m: string) => m,
|
|
100
|
+
backgroundColor: (m: string, _c: string) => m,
|
|
101
|
+
hyperlink: (t: string, _u: string) => t,
|
|
102
|
+
stdoutColumns: () => 80,
|
|
103
|
+
stderrColumns: () => 80,
|
|
104
|
+
print: () => Promise.resolve(),
|
|
105
|
+
debug: () => Promise.resolve(),
|
|
106
|
+
info: () => Promise.resolve(),
|
|
107
|
+
warn: () => Promise.resolve(),
|
|
108
|
+
error: () => Promise.resolve(),
|
|
109
|
+
setLevel: () => {},
|
|
110
|
+
getLevel: () => 1,
|
|
111
|
+
showSpinner: () => Promise.resolve(),
|
|
112
|
+
hideSpinner: () => Promise.resolve(),
|
|
113
|
+
showProgressBar: () => Promise.resolve(0),
|
|
114
|
+
hideProgressBar: () => Promise.resolve(),
|
|
115
|
+
hideAllProgressBars: () => Promise.resolve(),
|
|
116
|
+
updateProgressBar: () => {},
|
|
117
|
+
} as PrinterService;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createService(
|
|
121
|
+
keyReader: MockKeyReader,
|
|
122
|
+
configOverrides?: Partial<
|
|
123
|
+
import("../../../src/service/prompter/DefaultPrompterService.ts").PrompterServiceConfig
|
|
124
|
+
>,
|
|
125
|
+
): {
|
|
126
|
+
service: DefaultPrompterService;
|
|
127
|
+
terminal: MockTerminal;
|
|
128
|
+
} {
|
|
129
|
+
const terminal = new MockTerminal();
|
|
130
|
+
const config = { ...DEFAULT_PROMPTER_CONFIG, ...configOverrides };
|
|
131
|
+
const service = new DefaultPrompterService(
|
|
132
|
+
config,
|
|
133
|
+
terminal,
|
|
134
|
+
keyReader,
|
|
135
|
+
getMockPrinterService(),
|
|
136
|
+
);
|
|
137
|
+
return { service, terminal };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const SSH_ENV_KEYS = ["SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] as const;
|
|
141
|
+
|
|
142
|
+
function withSshEnvOverride<T>(
|
|
143
|
+
overrides: Record<string, string | undefined>,
|
|
144
|
+
fn: () => T,
|
|
145
|
+
): T {
|
|
146
|
+
const saved = SSH_ENV_KEYS.map((k) => process.env[k]);
|
|
147
|
+
SSH_ENV_KEYS.forEach((k) => {
|
|
148
|
+
if (k in overrides) {
|
|
149
|
+
if (overrides[k] === undefined) delete process.env[k];
|
|
150
|
+
else process.env[k] = overrides[k];
|
|
151
|
+
} else {
|
|
152
|
+
delete process.env[k];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
return fn();
|
|
157
|
+
} finally {
|
|
158
|
+
SSH_ENV_KEYS.forEach((k, i) => {
|
|
159
|
+
if (saved[i] === undefined) delete process.env[k];
|
|
160
|
+
else process.env[k] = saved[i];
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function withoutSshEnv<T>(fn: () => T): T {
|
|
166
|
+
return withSshEnvOverride({}, fn);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe("DefaultPrompterService tests", () => {
|
|
170
|
+
test("SINGLE_SELECT selects correct option", async () => {
|
|
171
|
+
const keyReader = new MockKeyReader();
|
|
172
|
+
keyReader.addKeys(
|
|
173
|
+
{ specialKey: SpecialKey.DOWN },
|
|
174
|
+
{ specialKey: SpecialKey.ENTER },
|
|
175
|
+
);
|
|
176
|
+
const { service } = createService(keyReader);
|
|
177
|
+
|
|
178
|
+
const result = await service.prompt({
|
|
179
|
+
name: "test",
|
|
180
|
+
promptText: "Pick one",
|
|
181
|
+
type: PromptType.SINGLE_SELECT,
|
|
182
|
+
options: [
|
|
183
|
+
{ displayValue: "Option A", returnedValue: "a" },
|
|
184
|
+
{ displayValue: "Option B", returnedValue: "b" },
|
|
185
|
+
{ displayValue: "Option C", returnedValue: "c" },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.name).toEqual("test");
|
|
190
|
+
expect(result.value).toEqual("b");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("MULTI_SELECT selects correct options", async () => {
|
|
194
|
+
const keyReader = new MockKeyReader();
|
|
195
|
+
keyReader.addKeys(
|
|
196
|
+
{ specialKey: SpecialKey.SPACE },
|
|
197
|
+
{ specialKey: SpecialKey.DOWN },
|
|
198
|
+
{ specialKey: SpecialKey.SPACE },
|
|
199
|
+
{ specialKey: SpecialKey.ENTER },
|
|
200
|
+
);
|
|
201
|
+
const { service } = createService(keyReader);
|
|
202
|
+
|
|
203
|
+
const result = await service.prompt({
|
|
204
|
+
name: "test",
|
|
205
|
+
promptText: "Pick many",
|
|
206
|
+
type: PromptType.MULTI_SELECT,
|
|
207
|
+
options: [
|
|
208
|
+
{ displayValue: "Option A", returnedValue: "a" },
|
|
209
|
+
{ displayValue: "Option B", returnedValue: "b" },
|
|
210
|
+
{ displayValue: "Option C", returnedValue: "c" },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result.name).toEqual("test");
|
|
215
|
+
expect(result.value).toEqual(["a", "b"]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("ACKNOWLEDGE selects Yes when navigated to", async () => {
|
|
219
|
+
const keyReader = new MockKeyReader();
|
|
220
|
+
keyReader.addKeys(
|
|
221
|
+
{ specialKey: SpecialKey.UP },
|
|
222
|
+
{ specialKey: SpecialKey.ENTER },
|
|
223
|
+
);
|
|
224
|
+
const { service } = createService(keyReader);
|
|
225
|
+
|
|
226
|
+
const result = await service.prompt({
|
|
227
|
+
name: "test",
|
|
228
|
+
promptText: "Confirm?",
|
|
229
|
+
type: PromptType.ACKNOWLEDGE,
|
|
230
|
+
options: [],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result.name).toEqual("test");
|
|
234
|
+
expect(result.value).toEqual(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("TOGGLE selects False", async () => {
|
|
238
|
+
const keyReader = new MockKeyReader();
|
|
239
|
+
keyReader.addKeys(
|
|
240
|
+
{ specialKey: SpecialKey.DOWN },
|
|
241
|
+
{ specialKey: SpecialKey.ENTER },
|
|
242
|
+
);
|
|
243
|
+
const { service } = createService(keyReader);
|
|
244
|
+
|
|
245
|
+
const result = await service.prompt({
|
|
246
|
+
name: "test",
|
|
247
|
+
promptText: "Toggle?",
|
|
248
|
+
type: PromptType.TOGGLE,
|
|
249
|
+
options: [],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.name).toEqual("test");
|
|
253
|
+
expect(result.value).toEqual(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("TEXT captures typed characters", async () => {
|
|
257
|
+
const keyReader = new MockKeyReader();
|
|
258
|
+
keyReader.addKeys(
|
|
259
|
+
{ key: "h" },
|
|
260
|
+
{ key: "i" },
|
|
261
|
+
{ specialKey: SpecialKey.ENTER },
|
|
262
|
+
);
|
|
263
|
+
const { service } = createService(keyReader);
|
|
264
|
+
|
|
265
|
+
const result = await service.prompt({
|
|
266
|
+
name: "test",
|
|
267
|
+
promptText: "Enter text",
|
|
268
|
+
type: PromptType.TEXT,
|
|
269
|
+
options: [],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(result.name).toEqual("test");
|
|
273
|
+
expect(result.value).toEqual("hi");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("TEXT handles backspace", async () => {
|
|
277
|
+
const keyReader = new MockKeyReader();
|
|
278
|
+
keyReader.addKeys(
|
|
279
|
+
{ key: "h" },
|
|
280
|
+
{ key: "i" },
|
|
281
|
+
{ specialKey: SpecialKey.BACKSPACE },
|
|
282
|
+
{ key: "o" },
|
|
283
|
+
{ specialKey: SpecialKey.ENTER },
|
|
284
|
+
);
|
|
285
|
+
const { service } = createService(keyReader);
|
|
286
|
+
|
|
287
|
+
const result = await service.prompt({
|
|
288
|
+
name: "test",
|
|
289
|
+
promptText: "Enter text",
|
|
290
|
+
type: PromptType.TEXT,
|
|
291
|
+
options: [],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(result.name).toEqual("test");
|
|
295
|
+
expect(result.value).toEqual("ho");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("promptEnabled=false with default returns default value", async () => {
|
|
299
|
+
const keyReader = new MockKeyReader();
|
|
300
|
+
const { service } = createService(keyReader);
|
|
301
|
+
service.promptEnabled = false;
|
|
302
|
+
|
|
303
|
+
const result = await service.prompt({
|
|
304
|
+
name: "test",
|
|
305
|
+
promptText: "Pick one",
|
|
306
|
+
type: PromptType.SINGLE_SELECT,
|
|
307
|
+
defaultOption: { displayValue: "Default", returnedValue: "default_val" },
|
|
308
|
+
options: [
|
|
309
|
+
{ displayValue: "Default", returnedValue: "default_val" },
|
|
310
|
+
{ displayValue: "Other", returnedValue: "other" },
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.name).toEqual("test");
|
|
315
|
+
expect(result.value).toEqual("default_val");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("promptEnabled=false without default throws error", async () => {
|
|
319
|
+
const keyReader = new MockKeyReader();
|
|
320
|
+
const { service } = createService(keyReader);
|
|
321
|
+
service.promptEnabled = false;
|
|
322
|
+
|
|
323
|
+
await expect(
|
|
324
|
+
service.prompt({
|
|
325
|
+
name: "test",
|
|
326
|
+
promptText: "Pick one",
|
|
327
|
+
type: PromptType.SINGLE_SELECT,
|
|
328
|
+
options: [
|
|
329
|
+
{ displayValue: "A", returnedValue: "a" },
|
|
330
|
+
],
|
|
331
|
+
}),
|
|
332
|
+
).rejects.toThrow("Prompting is disabled and no default for prompt: test");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("ESCAPE cancels prompt", async () => {
|
|
336
|
+
const keyReader = new MockKeyReader();
|
|
337
|
+
keyReader.addKeys({ specialKey: SpecialKey.ESCAPE });
|
|
338
|
+
const { service } = createService(keyReader);
|
|
339
|
+
|
|
340
|
+
await expect(
|
|
341
|
+
service.prompt({
|
|
342
|
+
name: "test",
|
|
343
|
+
promptText: "Pick one",
|
|
344
|
+
type: PromptType.SINGLE_SELECT,
|
|
345
|
+
options: [
|
|
346
|
+
{ displayValue: "A", returnedValue: "a" },
|
|
347
|
+
],
|
|
348
|
+
}),
|
|
349
|
+
).rejects.toThrow("Prompt cancelled");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("promptAll runs multiple prompts sequentially", async () => {
|
|
353
|
+
const keyReader = new MockKeyReader();
|
|
354
|
+
keyReader.addKeys(
|
|
355
|
+
{ specialKey: SpecialKey.ENTER },
|
|
356
|
+
{ key: "x" },
|
|
357
|
+
{ specialKey: SpecialKey.ENTER },
|
|
358
|
+
);
|
|
359
|
+
const { service } = createService(keyReader);
|
|
360
|
+
|
|
361
|
+
const results = await service.promptAll([
|
|
362
|
+
{
|
|
363
|
+
name: "first",
|
|
364
|
+
promptText: "Pick one",
|
|
365
|
+
type: PromptType.SINGLE_SELECT,
|
|
366
|
+
options: [
|
|
367
|
+
{ displayValue: "A", returnedValue: "a" },
|
|
368
|
+
{ displayValue: "B", returnedValue: "b" },
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "second",
|
|
373
|
+
promptText: "Enter text",
|
|
374
|
+
type: PromptType.TEXT,
|
|
375
|
+
options: [],
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
expect(results.length).toEqual(2);
|
|
380
|
+
expect(results[0]!.name).toEqual("first");
|
|
381
|
+
expect(results[0]!.value).toEqual("a");
|
|
382
|
+
expect(results[1]!.name).toEqual("second");
|
|
383
|
+
expect(results[1]!.value).toEqual("x");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("OPEN_URL calls openUrl and returns URL on ENTER", () =>
|
|
387
|
+
withoutSshEnv(async () => {
|
|
388
|
+
const keyReader = new MockKeyReader();
|
|
389
|
+
keyReader.addKeys({ specialKey: SpecialKey.ENTER });
|
|
390
|
+
let openedUrl: string | undefined;
|
|
391
|
+
const mockOpenUrl = (url: string): Promise<void> => {
|
|
392
|
+
openedUrl = url;
|
|
393
|
+
return Promise.resolve();
|
|
394
|
+
};
|
|
395
|
+
const { service } = createService(keyReader, { openUrl: mockOpenUrl });
|
|
396
|
+
|
|
397
|
+
const result = await service.prompt({
|
|
398
|
+
name: "login",
|
|
399
|
+
promptText: "Login at:",
|
|
400
|
+
type: PromptType.OPEN_URL,
|
|
401
|
+
options: [
|
|
402
|
+
{
|
|
403
|
+
displayValue: "Open browser to login",
|
|
404
|
+
returnedValue: "https://example.com/login",
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
expect(result.name).toEqual("login");
|
|
410
|
+
expect(result.value).toEqual("https://example.com/login");
|
|
411
|
+
expect(openedUrl).toEqual("https://example.com/login");
|
|
412
|
+
}));
|
|
413
|
+
|
|
414
|
+
test("OPEN_URL ESCAPE cancels without calling openUrl", async () => {
|
|
415
|
+
const keyReader = new MockKeyReader();
|
|
416
|
+
keyReader.addKeys({ specialKey: SpecialKey.ESCAPE });
|
|
417
|
+
let openUrlCalled = false;
|
|
418
|
+
const mockOpenUrl = (_url: string): Promise<void> => {
|
|
419
|
+
openUrlCalled = true;
|
|
420
|
+
return Promise.resolve();
|
|
421
|
+
};
|
|
422
|
+
const { service } = createService(keyReader, { openUrl: mockOpenUrl });
|
|
423
|
+
|
|
424
|
+
await expect(
|
|
425
|
+
service.prompt({
|
|
426
|
+
name: "login",
|
|
427
|
+
promptText: "Login at:",
|
|
428
|
+
type: PromptType.OPEN_URL,
|
|
429
|
+
options: [
|
|
430
|
+
{
|
|
431
|
+
displayValue: "Open browser",
|
|
432
|
+
returnedValue: "https://example.com/login",
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
}),
|
|
436
|
+
).rejects.toThrow("Prompt cancelled");
|
|
437
|
+
expect(openUrlCalled).toBe(false);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("OPEN_URL renders header, URL, and instruction", () =>
|
|
441
|
+
withoutSshEnv(async () => {
|
|
442
|
+
const keyReader = new MockKeyReader();
|
|
443
|
+
keyReader.addKeys({ specialKey: SpecialKey.ENTER });
|
|
444
|
+
const mockOpenUrl = (_url: string): Promise<void> => Promise.resolve();
|
|
445
|
+
const { service, terminal } = createService(keyReader, {
|
|
446
|
+
openUrl: mockOpenUrl,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await service.prompt({
|
|
450
|
+
name: "login",
|
|
451
|
+
promptText: "Login at:",
|
|
452
|
+
type: PromptType.OPEN_URL,
|
|
453
|
+
options: [
|
|
454
|
+
{
|
|
455
|
+
displayValue: "Open browser to login",
|
|
456
|
+
returnedValue: "https://example.com/login",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(terminal.buffer).toContain("Login at:");
|
|
462
|
+
expect(terminal.buffer).toContain("https://example.com/login");
|
|
463
|
+
expect(terminal.buffer).toContain(
|
|
464
|
+
"Press ENTER to open in the browser...",
|
|
465
|
+
);
|
|
466
|
+
expect(terminal.buffer).toContain("Open browser to login");
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
test("OPEN_URL handles openUrl failure gracefully", () =>
|
|
470
|
+
withoutSshEnv(async () => {
|
|
471
|
+
const keyReader = new MockKeyReader();
|
|
472
|
+
keyReader.addKeys({ specialKey: SpecialKey.ENTER });
|
|
473
|
+
const mockOpenUrl = (_url: string): Promise<void> => {
|
|
474
|
+
return Promise.reject(new Error("browser not found"));
|
|
475
|
+
};
|
|
476
|
+
const { service, terminal } = createService(keyReader, {
|
|
477
|
+
openUrl: mockOpenUrl,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const result = await service.prompt({
|
|
481
|
+
name: "login",
|
|
482
|
+
promptText: "Login at:",
|
|
483
|
+
type: PromptType.OPEN_URL,
|
|
484
|
+
options: [
|
|
485
|
+
{
|
|
486
|
+
displayValue: "Open browser",
|
|
487
|
+
returnedValue: "https://example.com/login",
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(result.name).toEqual("login");
|
|
493
|
+
expect(result.value).toEqual("https://example.com/login");
|
|
494
|
+
expect(terminal.buffer).toContain(
|
|
495
|
+
"Failed to open URL: browser not found",
|
|
496
|
+
);
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
test("OPEN_URL with promptEnabled=false and defaultOption returns default", async () => {
|
|
500
|
+
const keyReader = new MockKeyReader();
|
|
501
|
+
const { service } = createService(keyReader);
|
|
502
|
+
service.promptEnabled = false;
|
|
503
|
+
|
|
504
|
+
const result = await service.prompt({
|
|
505
|
+
name: "login",
|
|
506
|
+
promptText: "Login at:",
|
|
507
|
+
type: PromptType.OPEN_URL,
|
|
508
|
+
defaultOption: {
|
|
509
|
+
displayValue: "Default URL",
|
|
510
|
+
returnedValue: "https://example.com/default",
|
|
511
|
+
},
|
|
512
|
+
options: [
|
|
513
|
+
{
|
|
514
|
+
displayValue: "Open browser",
|
|
515
|
+
returnedValue: "https://example.com/login",
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
expect(result.name).toEqual("login");
|
|
521
|
+
expect(result.value).toEqual("https://example.com/default");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("OPEN_URL with promptEnabled=false and no defaultOption throws", async () => {
|
|
525
|
+
const keyReader = new MockKeyReader();
|
|
526
|
+
const { service } = createService(keyReader);
|
|
527
|
+
service.promptEnabled = false;
|
|
528
|
+
|
|
529
|
+
await expect(
|
|
530
|
+
service.prompt({
|
|
531
|
+
name: "login",
|
|
532
|
+
promptText: "Login at:",
|
|
533
|
+
type: PromptType.OPEN_URL,
|
|
534
|
+
options: [
|
|
535
|
+
{
|
|
536
|
+
displayValue: "Open browser",
|
|
537
|
+
returnedValue: "https://example.com/login",
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
}),
|
|
541
|
+
).rejects.toThrow(
|
|
542
|
+
"Prompting is disabled and no default for prompt: login",
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("OPEN_URL in SSH session shows copy message and skips openUrl", () =>
|
|
547
|
+
withSshEnvOverride(
|
|
548
|
+
{ SSH_CONNECTION: "192.168.1.1 12345 192.168.1.2 22" },
|
|
549
|
+
async () => {
|
|
550
|
+
const keyReader = new MockKeyReader();
|
|
551
|
+
keyReader.addKeys({ specialKey: SpecialKey.ENTER });
|
|
552
|
+
let openUrlCalled = false;
|
|
553
|
+
const mockOpenUrl = (_url: string): Promise<void> => {
|
|
554
|
+
openUrlCalled = true;
|
|
555
|
+
return Promise.resolve();
|
|
556
|
+
};
|
|
557
|
+
const { service, terminal } = createService(keyReader, {
|
|
558
|
+
openUrl: mockOpenUrl,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const result = await service.prompt({
|
|
562
|
+
name: "login",
|
|
563
|
+
promptText: "Login at:",
|
|
564
|
+
type: PromptType.OPEN_URL,
|
|
565
|
+
options: [
|
|
566
|
+
{
|
|
567
|
+
displayValue: "Open browser",
|
|
568
|
+
returnedValue: "https://example.com/login",
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
expect(result.name).toEqual("login");
|
|
574
|
+
expect(result.value).toEqual("https://example.com/login");
|
|
575
|
+
expect(openUrlCalled).toBe(false);
|
|
576
|
+
expect(terminal.buffer).toContain(
|
|
577
|
+
"Copy the URL above and open it in your local browser, then press ENTER to continue...",
|
|
578
|
+
);
|
|
579
|
+
expect(terminal.buffer).not.toContain(
|
|
580
|
+
"Press ENTER to open in the browser...",
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
));
|
|
584
|
+
|
|
585
|
+
test("OPEN_URL outside SSH session shows open message", () =>
|
|
586
|
+
withoutSshEnv(async () => {
|
|
587
|
+
const keyReader = new MockKeyReader();
|
|
588
|
+
keyReader.addKeys({ specialKey: SpecialKey.ENTER });
|
|
589
|
+
let openUrlCalled = false;
|
|
590
|
+
const mockOpenUrl = (_url: string): Promise<void> => {
|
|
591
|
+
openUrlCalled = true;
|
|
592
|
+
return Promise.resolve();
|
|
593
|
+
};
|
|
594
|
+
const { service, terminal } = createService(keyReader, {
|
|
595
|
+
openUrl: mockOpenUrl,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
await service.prompt({
|
|
599
|
+
name: "login",
|
|
600
|
+
promptText: "Login at:",
|
|
601
|
+
type: PromptType.OPEN_URL,
|
|
602
|
+
options: [
|
|
603
|
+
{
|
|
604
|
+
displayValue: "Open browser",
|
|
605
|
+
returnedValue: "https://example.com/login",
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(openUrlCalled).toBe(true);
|
|
611
|
+
expect(terminal.buffer).toContain(
|
|
612
|
+
"Press ENTER to open in the browser...",
|
|
613
|
+
);
|
|
614
|
+
}));
|
|
615
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import PrompterServiceProvider from "../../../src/service/prompter/PrompterServiceProvider.ts";
|
|
3
|
+
import { PROMPTER_SERVICE_ID } from "../../../src/api/service/core/PrompterService.ts";
|
|
4
|
+
import type PrompterService from "../../../src/api/service/core/PrompterService.ts";
|
|
5
|
+
import DefaultContext from "../../../src/runtime/DefaultContext.ts";
|
|
6
|
+
import { getCLIConfig } from "../../fixtures/CLIConfig.ts";
|
|
7
|
+
|
|
8
|
+
function getMockPrompterService(): PrompterService {
|
|
9
|
+
return {
|
|
10
|
+
promptEnabled: true,
|
|
11
|
+
prompt: () => Promise.resolve({ name: "", value: "" }),
|
|
12
|
+
promptAll: () => Promise.resolve([]),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("PrompterServiceProvider tests", () => {
|
|
17
|
+
test("PrompterServiceProvider has correct serviceId", () => {
|
|
18
|
+
const provider = new PrompterServiceProvider(
|
|
19
|
+
100,
|
|
20
|
+
getMockPrompterService(),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(provider.serviceId).toEqual(PROMPTER_SERVICE_ID);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("PrompterServiceProvider getServiceInfo returns service and one command", async () => {
|
|
27
|
+
const prompterService = getMockPrompterService();
|
|
28
|
+
const provider = new PrompterServiceProvider(100, prompterService);
|
|
29
|
+
const cliConfig = getCLIConfig();
|
|
30
|
+
|
|
31
|
+
const serviceInfo = await provider.getServiceInfo(cliConfig);
|
|
32
|
+
|
|
33
|
+
expect(serviceInfo.service).toBe(prompterService);
|
|
34
|
+
expect(serviceInfo.commands.length).toEqual(1);
|
|
35
|
+
expect(serviceInfo.commands[0]!.name).toEqual("no-prompt");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("PrompterServiceProvider initService resolves", async () => {
|
|
39
|
+
const provider = new PrompterServiceProvider(
|
|
40
|
+
100,
|
|
41
|
+
getMockPrompterService(),
|
|
42
|
+
);
|
|
43
|
+
const context = new DefaultContext(getCLIConfig());
|
|
44
|
+
|
|
45
|
+
await expect(provider.initService(context)).resolves.toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|