@flowscripter/dynamic-cli-framework 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.github/workflows/release-bun-library.yml +1 -0
  2. package/README/core-commands.md +40 -0
  3. package/README/core-service-providers.md +198 -0
  4. package/README/development.md +60 -0
  5. package/README/implementation-details.md +474 -0
  6. package/README/key-concepts.md +751 -0
  7. package/README.md +12 -1413
  8. package/bun.lock +20 -18
  9. package/index.ts +97 -1
  10. package/package.json +8 -7
  11. package/src/api/BaseCLIFeatureOptions.ts +10 -0
  12. package/src/api/CLIConfig.ts +5 -0
  13. package/src/api/RunResult.ts +16 -0
  14. package/src/api/argument/Argument.ts +24 -0
  15. package/src/api/service/ServiceProvider.ts +3 -3
  16. package/src/api/service/core/ArgumentPrompterService.ts +10 -0
  17. package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
  18. package/src/api/service/core/CompletionService.ts +36 -0
  19. package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
  20. package/src/api/service/core/ImagePrinterService.ts +26 -0
  21. package/src/api/service/core/KeyValueService.ts +22 -5
  22. package/src/api/service/core/PrinterService.ts +127 -1
  23. package/src/api/service/core/PrompterService.ts +49 -0
  24. package/src/api/service/core/SecretService.ts +27 -0
  25. package/src/api/service/core/ShutdownService.ts +17 -1
  26. package/src/api/service/core/Table.ts +85 -0
  27. package/src/api/service/core/TableGeneratorService.ts +38 -0
  28. package/src/api/service/core/TreePrinterService.ts +11 -0
  29. package/src/cli/BaseCLI.ts +116 -44
  30. package/src/cli/DefaultRuntimeCLI.ts +10 -18
  31. package/src/command/CompletionCommand.ts +179 -0
  32. package/src/command/MultiCommandCliHelpCommand.ts +18 -2
  33. package/src/command/SingleCommandCliHelpCommand.ts +10 -1
  34. package/src/launcher.ts +16 -58
  35. package/src/runtime/command/CommandValidator.ts +3 -3
  36. package/src/runtime/runner.ts +102 -30
  37. package/src/runtime/scanner.ts +18 -15
  38. package/src/runtime/values/argumentValueValidation.ts +49 -5
  39. package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
  40. package/src/runtime/values/subCommandValuePopulation.ts +9 -8
  41. package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
  42. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
  43. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
  44. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
  45. package/src/service/banner/BannerServiceProvider.ts +2 -2
  46. package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
  47. package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
  48. package/src/service/completion/CompletionServiceProvider.ts +212 -0
  49. package/src/service/completion/DefaultCompletionService.ts +364 -0
  50. package/src/service/completion/shell/ShellHandler.ts +16 -0
  51. package/src/service/completion/shell/bash.ts +47 -0
  52. package/src/service/completion/shell/fish.ts +43 -0
  53. package/src/service/completion/shell/powershell.ts +66 -0
  54. package/src/service/completion/shell/zsh.ts +50 -0
  55. package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
  56. package/src/service/configuration/DefaultKeyValueService.ts +53 -7
  57. package/src/service/configuration/DefaultSecretService.ts +85 -0
  58. package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
  59. package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
  60. package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
  61. package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
  62. package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
  63. package/src/service/printer/DefaultPrinterService.ts +147 -27
  64. package/src/service/printer/PrinterServiceProvider.ts +7 -7
  65. package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
  66. package/src/service/printer/terminal/Progress.ts +21 -4
  67. package/src/service/printer/terminal/Spinner.ts +32 -8
  68. package/src/service/printer/terminal/Theme.ts +1 -1
  69. package/src/service/prompter/DefaultPrompterService.ts +87 -0
  70. package/src/service/prompter/PrompterServiceProvider.ts +36 -0
  71. package/src/service/prompter/command/NoPromptCommand.ts +40 -0
  72. package/src/service/prompter/prompt/PromptContext.ts +47 -0
  73. package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
  74. package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
  75. package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
  76. package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
  77. package/src/service/prompter/prompt/promptText.ts +107 -0
  78. package/src/service/prompter/prompt/promptToggle.ts +28 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +13 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
  81. package/src/service/shutdown/ShutdownState.ts +5 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
  83. package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
  84. package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
  85. package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
  86. package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
  87. package/src/terminal/Ansi.ts +27 -0
  88. package/src/terminal/ImageRenderer.ts +199 -0
  89. package/src/terminal/KeyReader.ts +23 -0
  90. package/src/terminal/PngDecoder.ts +143 -0
  91. package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
  92. package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
  93. package/src/terminal/TtyKeyReader.ts +83 -0
  94. package/src/terminal/TtyStyler.ts +78 -0
  95. package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
  96. package/src/terminal/supportsHyperlinks.ts +110 -0
  97. package/src/util/configHelper.ts +2 -2
  98. package/src/util/envVarHelper.ts +2 -2
  99. package/src/util/helpHelper.ts +66 -122
  100. package/src/util/logger.ts +16 -14
  101. package/src/util/runnerHelper.ts +1 -1
  102. package/tests/cli/BaseCLI.test.ts +50 -54
  103. package/tests/command/CompletionCommand.test.ts +68 -0
  104. package/tests/command/UsageCommand.test.ts +3 -2
  105. package/tests/command/VersionCommand.test.ts +3 -2
  106. package/tests/fixtures/Context.ts +9 -2
  107. package/tests/fixtures/util.ts +1 -1
  108. package/tests/runtime/parser.test.ts +9 -7
  109. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
  110. package/tests/runtime/values/argumentValueValidation.test.ts +193 -0
  111. package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
  112. package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
  113. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
  114. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
  115. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
  116. package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
  117. package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
  118. package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
  119. package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
  120. package/tests/service/completion/shell/bash.test.ts +73 -0
  121. package/tests/service/completion/shell/fish.test.ts +62 -0
  122. package/tests/service/completion/shell/powershell.test.ts +59 -0
  123. package/tests/service/completion/shell/zsh.test.ts +60 -0
  124. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
  125. package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
  126. package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
  127. package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
  128. package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
  129. package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
  130. package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
  131. package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
  132. package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
  133. package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
  134. package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
  135. package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
  136. package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
  137. package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
  138. package/tests/service/printer/command/LogLevelCommand.test.ts +7 -5
  139. package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
  140. package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
  141. package/tests/service/printer/terminal/Progress.test.ts +64 -4
  142. package/tests/service/printer/terminal/Spinner.test.ts +44 -3
  143. package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
  144. package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
  145. package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
  146. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
  147. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
  148. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
  149. package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
  150. package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
  151. package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
  152. package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
  153. package/tests/terminal/ImageRenderer.test.ts +161 -0
  154. package/tests/terminal/PngDecoder.test.ts +223 -0
  155. package/tests/terminal/supportsHyperlinks.test.ts +149 -0
  156. package/tests/util/configHelper.test.ts +4 -4
  157. package/tests/util/logger.test.ts +23 -1
  158. package/tsconfig.json +31 -0
  159. package/src/service/printer/terminal/Ansi.ts +0 -10
  160. package/src/service/printer/terminal/TtyStyler.ts +0 -39
@@ -0,0 +1,404 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import CompletionServiceProvider from "../../../src/service/completion/CompletionServiceProvider.ts";
3
+ import DefaultCompletionService from "../../../src/service/completion/DefaultCompletionService.ts";
4
+ import { COMPLETION_SERVICE_ID } from "../../../src/api/service/core/CompletionService.ts";
5
+ import DefaultContext from "../../../src/runtime/DefaultContext.ts";
6
+ import type CLIConfig from "../../../src/api/CLIConfig.ts";
7
+
8
+ function getCLIConfig(): CLIConfig {
9
+ return { name: "testcli", description: "Test CLI", version: "1.0.0" };
10
+ }
11
+
12
+ describe("CompletionServiceProvider", () => {
13
+ test("has correct serviceId", () => {
14
+ const service = new DefaultCompletionService();
15
+ const provider = new CompletionServiceProvider(60, service);
16
+ expect(provider.serviceId).toEqual(COMPLETION_SERVICE_ID);
17
+ });
18
+
19
+ test("has correct servicePriority", () => {
20
+ const service = new DefaultCompletionService();
21
+ const provider = new CompletionServiceProvider(60, service);
22
+ expect(provider.servicePriority).toEqual(60);
23
+ });
24
+
25
+ test("getServiceInfo returns service and no commands", async () => {
26
+ const service = new DefaultCompletionService();
27
+ const provider = new CompletionServiceProvider(60, service);
28
+ const serviceInfo = await provider.getServiceInfo(getCLIConfig());
29
+
30
+ expect(serviceInfo.service).toBe(service);
31
+ expect(serviceInfo.commands.length).toEqual(0);
32
+ });
33
+
34
+ test("initService resolves when prompter service not available", async () => {
35
+ const service = new DefaultCompletionService();
36
+ const provider = new CompletionServiceProvider(60, service);
37
+ await provider.getServiceInfo(getCLIConfig());
38
+
39
+ const context = new DefaultContext(getCLIConfig());
40
+ await expect(provider.initService(context)).resolves.toBeUndefined();
41
+ });
42
+
43
+ test("initService resolves when key-value service not available", async () => {
44
+ const service = new DefaultCompletionService();
45
+ const provider = new CompletionServiceProvider(60, service);
46
+ await provider.getServiceInfo(getCLIConfig());
47
+
48
+ const context = new DefaultContext(getCLIConfig());
49
+ context.addServiceInstance(
50
+ "@flowscripter/dynamic-cli-framework/prompter-service",
51
+ {
52
+ promptEnabled: true,
53
+ prompt: () => Promise.resolve({ name: "", value: false }),
54
+ promptAll: () => Promise.resolve([]),
55
+ },
56
+ );
57
+ await expect(provider.initService(context)).resolves.toBeUndefined();
58
+ });
59
+
60
+ test("initService skips when completion-status is 'installed'", async () => {
61
+ const service = new DefaultCompletionService();
62
+ const provider = new CompletionServiceProvider(60, service);
63
+ await provider.getServiceInfo(getCLIConfig());
64
+
65
+ const context = new DefaultContext(getCLIConfig());
66
+ context.addServiceInstance(
67
+ "@flowscripter/dynamic-cli-framework/prompter-service",
68
+ {
69
+ promptEnabled: true,
70
+ prompt: () => Promise.resolve({ name: "", value: false }),
71
+ promptAll: () => Promise.resolve([]),
72
+ },
73
+ );
74
+ context.addServiceInstance(
75
+ "@flowscripter/dynamic-cli-framework/key-value-service",
76
+ {
77
+ hasKey: (key: string) => Promise.resolve(key === "completion-status"),
78
+ getKey: () => Promise.resolve("installed"),
79
+ setKey: () => Promise.resolve(),
80
+ deleteKey: () => Promise.resolve(),
81
+ },
82
+ );
83
+ await expect(provider.initService(context)).resolves.toBeUndefined();
84
+ });
85
+
86
+ test("initService skips when completion-status is 'declined'", async () => {
87
+ const service = new DefaultCompletionService();
88
+ const provider = new CompletionServiceProvider(60, service);
89
+ await provider.getServiceInfo(getCLIConfig());
90
+
91
+ const context = new DefaultContext(getCLIConfig());
92
+ context.addServiceInstance(
93
+ "@flowscripter/dynamic-cli-framework/prompter-service",
94
+ {
95
+ promptEnabled: true,
96
+ prompt: () => Promise.resolve({ name: "", value: false }),
97
+ promptAll: () => Promise.resolve([]),
98
+ },
99
+ );
100
+ context.addServiceInstance(
101
+ "@flowscripter/dynamic-cli-framework/key-value-service",
102
+ {
103
+ hasKey: (key: string) => Promise.resolve(key === "completion-status"),
104
+ getKey: () => Promise.resolve("declined"),
105
+ setKey: () => Promise.resolve(),
106
+ deleteKey: () => Promise.resolve(),
107
+ },
108
+ );
109
+ await expect(provider.initService(context)).resolves.toBeUndefined();
110
+ });
111
+
112
+ test("initService skips when promptEnabled is false", async () => {
113
+ const service = new DefaultCompletionService();
114
+ const provider = new CompletionServiceProvider(60, service);
115
+ await provider.getServiceInfo(getCLIConfig());
116
+
117
+ const context = new DefaultContext(getCLIConfig());
118
+ context.addServiceInstance(
119
+ "@flowscripter/dynamic-cli-framework/prompter-service",
120
+ {
121
+ promptEnabled: false,
122
+ prompt: () => Promise.resolve({ name: "", value: false }),
123
+ promptAll: () => Promise.resolve([]),
124
+ },
125
+ );
126
+ context.addServiceInstance(
127
+ "@flowscripter/dynamic-cli-framework/key-value-service",
128
+ {
129
+ hasKey: () => Promise.resolve(false),
130
+ getKey: () => Promise.resolve(""),
131
+ setKey: () => Promise.resolve(),
132
+ deleteKey: () => Promise.resolve(),
133
+ },
134
+ );
135
+ await expect(provider.initService(context)).resolves.toBeUndefined();
136
+ });
137
+
138
+ test("initService skips when no supported shells detected", async () => {
139
+ const service = new DefaultCompletionService();
140
+ service.validateShellEnvironment = () => Promise.resolve(false);
141
+ const provider = new CompletionServiceProvider(60, service);
142
+ await provider.getServiceInfo(getCLIConfig());
143
+
144
+ const context = new DefaultContext(getCLIConfig());
145
+
146
+ let promptCalled = false;
147
+ context.addServiceInstance(
148
+ "@flowscripter/dynamic-cli-framework/prompter-service",
149
+ {
150
+ promptEnabled: true,
151
+ prompt: () => {
152
+ promptCalled = true;
153
+ return Promise.resolve({ name: "", value: false });
154
+ },
155
+ promptAll: () => Promise.resolve([]),
156
+ },
157
+ );
158
+ context.addServiceInstance(
159
+ "@flowscripter/dynamic-cli-framework/key-value-service",
160
+ {
161
+ hasKey: () => Promise.resolve(false),
162
+ getKey: () => Promise.resolve(""),
163
+ setKey: () => Promise.resolve(),
164
+ deleteKey: () => Promise.resolve(),
165
+ },
166
+ );
167
+
168
+ await provider.initService(context);
169
+ expect(promptCalled).toBe(false);
170
+ });
171
+
172
+ test("initService stores 'declined' when user says no to completion", async () => {
173
+ const service = new DefaultCompletionService();
174
+ service.validateShellEnvironment = () => Promise.resolve(true);
175
+ const provider = new CompletionServiceProvider(60, service);
176
+ await provider.getServiceInfo(getCLIConfig());
177
+
178
+ const context = new DefaultContext(getCLIConfig());
179
+
180
+ let storedKey = "";
181
+ let storedValue = "";
182
+ context.addServiceInstance(
183
+ "@flowscripter/dynamic-cli-framework/prompter-service",
184
+ {
185
+ promptEnabled: true,
186
+ prompt: () =>
187
+ Promise.resolve({ name: "enable-completion", value: false }),
188
+ promptAll: () => Promise.resolve([]),
189
+ },
190
+ );
191
+ context.addServiceInstance(
192
+ "@flowscripter/dynamic-cli-framework/key-value-service",
193
+ {
194
+ hasKey: () => Promise.resolve(false),
195
+ getKey: () => Promise.resolve(""),
196
+ setKey: (key: string, value: string) => {
197
+ storedKey = key;
198
+ storedValue = value;
199
+ return Promise.resolve();
200
+ },
201
+ deleteKey: () => Promise.resolve(),
202
+ },
203
+ );
204
+
205
+ await provider.initService(context);
206
+ expect(storedKey).toEqual("completion-status");
207
+ expect(storedValue).toEqual("declined");
208
+ });
209
+
210
+ test("initService skips shell prompt when only one shell detected", async () => {
211
+ const service = new DefaultCompletionService();
212
+ service.validateShellEnvironment = (shell) =>
213
+ Promise.resolve(shell === "bash");
214
+ const provider = new CompletionServiceProvider(60, service);
215
+ await provider.getServiceInfo(getCLIConfig());
216
+
217
+ const context = new DefaultContext(getCLIConfig());
218
+
219
+ const storedEntries: Array<{ key: string; value: string }> = [];
220
+ const promptNames: string[] = [];
221
+
222
+ context.addServiceInstance(
223
+ "@flowscripter/dynamic-cli-framework/prompter-service",
224
+ {
225
+ promptEnabled: true,
226
+ prompt: (promptDef: { name: string }) => {
227
+ promptNames.push(promptDef.name);
228
+ return Promise.resolve({
229
+ name: promptDef.name,
230
+ value: true,
231
+ });
232
+ },
233
+ promptAll: () => Promise.resolve([]),
234
+ },
235
+ );
236
+ context.addServiceInstance(
237
+ "@flowscripter/dynamic-cli-framework/key-value-service",
238
+ {
239
+ hasKey: () => Promise.resolve(false),
240
+ getKey: () => Promise.resolve(""),
241
+ setKey: (key: string, value: string) => {
242
+ storedEntries.push({ key, value });
243
+ return Promise.resolve();
244
+ },
245
+ deleteKey: () => Promise.resolve(),
246
+ },
247
+ );
248
+
249
+ let infoMessage = "";
250
+ context.addServiceInstance(
251
+ "@flowscripter/dynamic-cli-framework/printer-service",
252
+ {
253
+ info: (msg: string) => {
254
+ infoMessage = msg;
255
+ return Promise.resolve();
256
+ },
257
+ warn: () => Promise.resolve(),
258
+ error: () => Promise.resolve(),
259
+ print: () => Promise.resolve(),
260
+ },
261
+ );
262
+
263
+ await provider.initService(context);
264
+
265
+ expect(promptNames).toEqual(["enable-completion"]);
266
+ const statusEntry = storedEntries.find(
267
+ (e) => e.key === "completion-status",
268
+ );
269
+ expect(statusEntry).toBeDefined();
270
+ expect(statusEntry!.value).toEqual("installed");
271
+ expect(infoMessage).toContain("Shell completion installed for bash");
272
+ });
273
+
274
+ test("initService shows shell prompt with default when multiple shells detected", async () => {
275
+ const service = new DefaultCompletionService();
276
+ service.validateShellEnvironment = (shell) =>
277
+ Promise.resolve(shell === "bash" || shell === "zsh");
278
+ const provider = new CompletionServiceProvider(60, service);
279
+ await provider.getServiceInfo(getCLIConfig());
280
+
281
+ const context = new DefaultContext(getCLIConfig());
282
+
283
+ const storedEntries: Array<{ key: string; value: string }> = [];
284
+ const promptResponses = new Map<string, unknown>();
285
+ promptResponses.set("enable-completion", true);
286
+ promptResponses.set("shell-type", "zsh");
287
+
288
+ let shellPromptDefault: unknown;
289
+ context.addServiceInstance(
290
+ "@flowscripter/dynamic-cli-framework/prompter-service",
291
+ {
292
+ promptEnabled: true,
293
+ prompt: (promptDef: {
294
+ name: string;
295
+ defaultOption?: { returnedValue: unknown };
296
+ }) => {
297
+ if (promptDef.name === "shell-type") {
298
+ shellPromptDefault = promptDef.defaultOption;
299
+ }
300
+ return Promise.resolve({
301
+ name: promptDef.name,
302
+ value: promptResponses.get(promptDef.name),
303
+ });
304
+ },
305
+ promptAll: () => Promise.resolve([]),
306
+ },
307
+ );
308
+ context.addServiceInstance(
309
+ "@flowscripter/dynamic-cli-framework/key-value-service",
310
+ {
311
+ hasKey: () => Promise.resolve(false),
312
+ getKey: () => Promise.resolve(""),
313
+ setKey: (key: string, value: string) => {
314
+ storedEntries.push({ key, value });
315
+ return Promise.resolve();
316
+ },
317
+ deleteKey: () => Promise.resolve(),
318
+ },
319
+ );
320
+
321
+ let infoMessage = "";
322
+ context.addServiceInstance(
323
+ "@flowscripter/dynamic-cli-framework/printer-service",
324
+ {
325
+ info: (msg: string) => {
326
+ infoMessage = msg;
327
+ return Promise.resolve();
328
+ },
329
+ warn: () => Promise.resolve(),
330
+ error: () => Promise.resolve(),
331
+ print: () => Promise.resolve(),
332
+ },
333
+ );
334
+
335
+ await provider.initService(context);
336
+
337
+ expect(shellPromptDefault).toEqual({
338
+ displayValue: "bash",
339
+ returnedValue: "bash",
340
+ });
341
+ const statusEntry = storedEntries.find(
342
+ (e) => e.key === "completion-status",
343
+ );
344
+ expect(statusEntry).toBeDefined();
345
+ expect(statusEntry!.value).toEqual("installed");
346
+ expect(infoMessage).toContain("Shell completion installed for zsh");
347
+ });
348
+
349
+ test("initService shows error when installation fails", async () => {
350
+ const service = new DefaultCompletionService();
351
+ service.validateShellEnvironment = () => Promise.resolve(true);
352
+ const originalGetConfigPath = service.getDefaultConfigPath.bind(service);
353
+ service.getDefaultConfigPath = () => {
354
+ throw new Error("test error");
355
+ };
356
+
357
+ const provider = new CompletionServiceProvider(60, service);
358
+ await provider.getServiceInfo(getCLIConfig());
359
+
360
+ const context = new DefaultContext(getCLIConfig());
361
+
362
+ context.addServiceInstance(
363
+ "@flowscripter/dynamic-cli-framework/prompter-service",
364
+ {
365
+ promptEnabled: true,
366
+ prompt: (promptDef: { name: string }) =>
367
+ Promise.resolve({
368
+ name: promptDef.name,
369
+ value: promptDef.name === "enable-completion" ? true : "bash",
370
+ }),
371
+ promptAll: () => Promise.resolve([]),
372
+ },
373
+ );
374
+ context.addServiceInstance(
375
+ "@flowscripter/dynamic-cli-framework/key-value-service",
376
+ {
377
+ hasKey: () => Promise.resolve(false),
378
+ getKey: () => Promise.resolve(""),
379
+ setKey: () => Promise.resolve(),
380
+ deleteKey: () => Promise.resolve(),
381
+ },
382
+ );
383
+
384
+ let errorMessage = "";
385
+ context.addServiceInstance(
386
+ "@flowscripter/dynamic-cli-framework/printer-service",
387
+ {
388
+ info: () => Promise.resolve(),
389
+ warn: () => Promise.resolve(),
390
+ error: (msg: string) => {
391
+ errorMessage = msg;
392
+ return Promise.resolve();
393
+ },
394
+ print: () => Promise.resolve(),
395
+ },
396
+ );
397
+
398
+ await provider.initService(context);
399
+ expect(errorMessage).toContain("Failed to install completion");
400
+ expect(errorMessage).toContain("test error");
401
+
402
+ service.getDefaultConfigPath = originalGetConfigPath;
403
+ });
404
+ });
@@ -0,0 +1,278 @@
1
+ import path from "node:path";
2
+ import { describe, expect, test } from "bun:test";
3
+ import DefaultCompletionService from "../../../src/service/completion/DefaultCompletionService.ts";
4
+ import DefaultCommandRegistry from "../../../src/runtime/registry/DefaultCommandRegistry.ts";
5
+ import { ShellType } from "../../../src/api/service/core/CompletionService.ts";
6
+ import type SubCommand from "../../../src/api/command/SubCommand.ts";
7
+ import type GroupCommand from "../../../src/api/command/GroupCommand.ts";
8
+ import type GlobalCommand from "../../../src/api/command/GlobalCommand.ts";
9
+ import type GlobalModifierCommand from "../../../src/api/command/GlobalModifierCommand.ts";
10
+ import { ArgumentValueTypeName } from "../../../src/api/argument/ArgumentValueTypes.ts";
11
+ import type Context from "../../../src/api/Context.ts";
12
+
13
+ function makeSubCommand(
14
+ name: string,
15
+ description = "",
16
+ options: SubCommand["options"] = [],
17
+ positionals: SubCommand["positionals"] = [],
18
+ ): SubCommand {
19
+ return {
20
+ name,
21
+ description,
22
+ enableConfiguration: false,
23
+ options,
24
+ positionals,
25
+ execute: () => Promise.resolve(),
26
+ };
27
+ }
28
+
29
+ function makeGroupCommand(
30
+ name: string,
31
+ description: string,
32
+ members: SubCommand[],
33
+ ): GroupCommand {
34
+ return {
35
+ name,
36
+ description,
37
+ enableConfiguration: false,
38
+ memberSubCommands: members,
39
+ execute: (_context: Context) => Promise.resolve(),
40
+ };
41
+ }
42
+
43
+ function makeGlobalCommand(
44
+ name: string,
45
+ description: string,
46
+ ): GlobalCommand {
47
+ return {
48
+ name,
49
+ description,
50
+ enableConfiguration: false,
51
+ argument: { type: ArgumentValueTypeName.BOOLEAN },
52
+ execute: () => Promise.resolve(),
53
+ };
54
+ }
55
+
56
+ function makeGlobalModifierCommand(
57
+ name: string,
58
+ description: string,
59
+ ): GlobalModifierCommand {
60
+ return {
61
+ name,
62
+ description,
63
+ enableConfiguration: false,
64
+ argument: { type: ArgumentValueTypeName.BOOLEAN },
65
+ executePriority: 0,
66
+ execute: () => Promise.resolve(),
67
+ };
68
+ }
69
+
70
+ describe("DefaultCompletionService", () => {
71
+ function createService(
72
+ commands: Array<
73
+ SubCommand | GroupCommand | GlobalCommand | GlobalModifierCommand
74
+ >,
75
+ ): DefaultCompletionService {
76
+ const service = new DefaultCompletionService();
77
+ const registry = new DefaultCommandRegistry(commands);
78
+ service.setCommandRegistry(registry);
79
+ return service;
80
+ }
81
+
82
+ test("returns empty completions if no registry set", async () => {
83
+ const service = new DefaultCompletionService();
84
+ const result = await service.generateCompletions(
85
+ ShellType.BASH,
86
+ "mycli ",
87
+ 6,
88
+ );
89
+ expect(result).toEqual([]);
90
+ });
91
+
92
+ test("completes command names from prefix", async () => {
93
+ const service = createService([
94
+ makeSubCommand("help", "Display help"),
95
+ makeSubCommand("history", "Show history"),
96
+ makeSubCommand("run", "Run something"),
97
+ ]);
98
+
99
+ const result = await service.generateCompletions(
100
+ ShellType.BASH,
101
+ "mycli h",
102
+ 7,
103
+ );
104
+ expect(result.length).toEqual(2);
105
+ expect(result.map((r) => r.value)).toContain("help");
106
+ expect(result.map((r) => r.value)).toContain("history");
107
+ });
108
+
109
+ test("completes all commands when no prefix", async () => {
110
+ const service = createService([
111
+ makeSubCommand("help", "Display help"),
112
+ makeSubCommand("run", "Run something"),
113
+ ]);
114
+
115
+ const result = await service.generateCompletions(
116
+ ShellType.BASH,
117
+ "mycli ",
118
+ 6,
119
+ );
120
+ expect(result.map((r) => r.value)).toContain("help");
121
+ expect(result.map((r) => r.value)).toContain("run");
122
+ });
123
+
124
+ test("completes group:member joined names", async () => {
125
+ const members = [
126
+ makeSubCommand("integration", "Install completion"),
127
+ makeSubCommand("complete", "Generate completions"),
128
+ ];
129
+ const group = makeGroupCommand(
130
+ "completions",
131
+ "Completion management",
132
+ members,
133
+ );
134
+ const service = createService([group]);
135
+
136
+ const result = await service.generateCompletions(
137
+ ShellType.BASH,
138
+ "mycli comp",
139
+ 10,
140
+ );
141
+ const values = result.map((r) => r.value);
142
+ expect(values).toContain("completions");
143
+ expect(values).toContain("completions:integration");
144
+ expect(values).toContain("completions:complete");
145
+ });
146
+
147
+ test("completes member sub-commands after group:", async () => {
148
+ const members = [
149
+ makeSubCommand("integration", "Install completion"),
150
+ makeSubCommand("complete", "Generate completions"),
151
+ ];
152
+ const group = makeGroupCommand(
153
+ "completions",
154
+ "Completion management",
155
+ members,
156
+ );
157
+ const service = createService([group]);
158
+
159
+ const result = await service.generateCompletions(
160
+ ShellType.BASH,
161
+ "mycli completions:i",
162
+ 19,
163
+ );
164
+ const values = result.map((r) => r.value);
165
+ expect(values).toContain("completions:integration");
166
+ expect(values).not.toContain("completions:complete");
167
+ });
168
+
169
+ test("completes sub-command options", async () => {
170
+ const cmd = makeSubCommand("deploy", "Deploy app", [
171
+ {
172
+ name: "target",
173
+ type: ArgumentValueTypeName.STRING,
174
+ description: "Deploy target",
175
+ isOptional: true,
176
+ },
177
+ {
178
+ name: "force",
179
+ type: ArgumentValueTypeName.BOOLEAN,
180
+ description: "Force deploy",
181
+ isOptional: true,
182
+ shortAlias: "f",
183
+ },
184
+ ]);
185
+ const service = createService([cmd]);
186
+
187
+ const result = await service.generateCompletions(
188
+ ShellType.BASH,
189
+ "mycli deploy --t",
190
+ 16,
191
+ );
192
+ const values = result.map((r) => r.value);
193
+ expect(values).toContain("--target");
194
+ });
195
+
196
+ test("completes option allowable values", async () => {
197
+ const cmd = makeSubCommand("deploy", "Deploy app", [
198
+ {
199
+ name: "env",
200
+ type: ArgumentValueTypeName.STRING,
201
+ description: "Environment",
202
+ isOptional: true,
203
+ allowableValues: ["staging", "production", "development"],
204
+ },
205
+ ]);
206
+ const service = createService([cmd]);
207
+
208
+ const result = await service.generateCompletions(
209
+ ShellType.BASH,
210
+ "mycli deploy --env st",
211
+ 21,
212
+ );
213
+ const values = result.map((r) => r.value);
214
+ expect(values).toContain("staging");
215
+ expect(values).not.toContain("production");
216
+ });
217
+
218
+ test("completes global modifier commands with -- prefix", async () => {
219
+ const modifier = makeGlobalModifierCommand(
220
+ "verbose",
221
+ "Enable verbose output",
222
+ );
223
+ const cmd = makeSubCommand("run", "Run something");
224
+ const service = createService([modifier, cmd]);
225
+
226
+ const result = await service.generateCompletions(
227
+ ShellType.BASH,
228
+ "mycli --v",
229
+ 9,
230
+ );
231
+ const values = result.map((r) => r.value);
232
+ expect(values).toContain("--verbose");
233
+ });
234
+
235
+ test("completes global commands with -- prefix", async () => {
236
+ const global = makeGlobalCommand("version", "Show version");
237
+ const cmd = makeSubCommand("run", "Run something");
238
+ const service = createService([global, cmd]);
239
+
240
+ const result = await service.generateCompletions(
241
+ ShellType.BASH,
242
+ "mycli --ver",
243
+ 11,
244
+ );
245
+ const values = result.map((r) => r.value);
246
+ expect(values).toContain("--version");
247
+ });
248
+
249
+ test("formatCompletions delegates to shell handler", () => {
250
+ const service = new DefaultCompletionService();
251
+ const result = service.formatCompletions(ShellType.FISH, [
252
+ { value: "help", description: "Display help" },
253
+ ]);
254
+ expect(result).toEqual("help\tDisplay help");
255
+ });
256
+
257
+ test("getBootstrapScript delegates to shell handler", () => {
258
+ const service = new DefaultCompletionService();
259
+ const result = service.getBootstrapScript(ShellType.BASH, "mycli");
260
+ expect(result).toContain("_mycli_completions");
261
+ });
262
+
263
+ test("getDefaultConfigPath delegates to shell handler", () => {
264
+ const service = new DefaultCompletionService();
265
+ const result = service.getDefaultConfigPath(ShellType.ZSH);
266
+ expect(result).toEndWith(path.sep + ".zshrc");
267
+ });
268
+
269
+ test("parseCompletionContext delegates to shell handler", () => {
270
+ const service = new DefaultCompletionService();
271
+ const result = service.parseCompletionContext(ShellType.BASH, [
272
+ "mycli help",
273
+ "10",
274
+ ]);
275
+ expect(result.line).toEqual("mycli help");
276
+ expect(result.cursorPosition).toEqual(10);
277
+ });
278
+ });