@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.
Files changed (165) 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 +98 -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 +40 -1
  15. package/src/api/argument/ArgumentValueTypes.ts +1 -1
  16. package/src/api/command/SubCommand.ts +1 -1
  17. package/src/api/service/ServiceProvider.ts +3 -3
  18. package/src/api/service/core/ArgumentPrompterService.ts +10 -0
  19. package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
  20. package/src/api/service/core/CompletionService.ts +36 -0
  21. package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
  22. package/src/api/service/core/ImagePrinterService.ts +26 -0
  23. package/src/api/service/core/KeyValueService.ts +22 -5
  24. package/src/api/service/core/PrinterService.ts +127 -1
  25. package/src/api/service/core/PrompterService.ts +49 -0
  26. package/src/api/service/core/SecretService.ts +27 -0
  27. package/src/api/service/core/ShutdownService.ts +17 -1
  28. package/src/api/service/core/Table.ts +85 -0
  29. package/src/api/service/core/TableGeneratorService.ts +38 -0
  30. package/src/api/service/core/TreePrinterService.ts +11 -0
  31. package/src/cli/BaseCLI.ts +116 -44
  32. package/src/cli/DefaultRuntimeCLI.ts +10 -18
  33. package/src/command/CompletionCommand.ts +179 -0
  34. package/src/command/MultiCommandCliHelpCommand.ts +20 -32
  35. package/src/command/SingleCommandCliHelpCommand.ts +10 -1
  36. package/src/launcher.ts +16 -58
  37. package/src/runtime/command/CommandValidator.ts +70 -6
  38. package/src/runtime/runner.ts +112 -31
  39. package/src/runtime/scanner.ts +18 -15
  40. package/src/runtime/values/argumentValueValidation.ts +93 -10
  41. package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
  42. package/src/runtime/values/subCommandValuePopulation.ts +9 -8
  43. package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
  44. package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
  45. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
  46. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
  47. package/src/service/banner/BannerServiceProvider.ts +2 -2
  48. package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
  49. package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
  50. package/src/service/completion/CompletionServiceProvider.ts +212 -0
  51. package/src/service/completion/DefaultCompletionService.ts +364 -0
  52. package/src/service/completion/shell/ShellHandler.ts +16 -0
  53. package/src/service/completion/shell/bash.ts +47 -0
  54. package/src/service/completion/shell/fish.ts +43 -0
  55. package/src/service/completion/shell/powershell.ts +66 -0
  56. package/src/service/completion/shell/zsh.ts +50 -0
  57. package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
  58. package/src/service/configuration/DefaultKeyValueService.ts +53 -7
  59. package/src/service/configuration/DefaultSecretService.ts +85 -0
  60. package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
  61. package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
  62. package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
  63. package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
  64. package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
  65. package/src/service/printer/DefaultPrinterService.ts +147 -27
  66. package/src/service/printer/PrinterServiceProvider.ts +7 -7
  67. package/src/service/printer/command/LogLevelCommand.ts +2 -1
  68. package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
  69. package/src/service/printer/terminal/Progress.ts +21 -4
  70. package/src/service/printer/terminal/Spinner.ts +32 -8
  71. package/src/service/printer/terminal/Theme.ts +1 -1
  72. package/src/service/prompter/DefaultPrompterService.ts +87 -0
  73. package/src/service/prompter/PrompterServiceProvider.ts +36 -0
  74. package/src/service/prompter/command/NoPromptCommand.ts +40 -0
  75. package/src/service/prompter/prompt/PromptContext.ts +47 -0
  76. package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
  77. package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
  78. package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
  79. package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
  80. package/src/service/prompter/prompt/promptText.ts +107 -0
  81. package/src/service/prompter/prompt/promptToggle.ts +28 -0
  82. package/src/service/shutdown/DefaultShutdownService.ts +13 -0
  83. package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
  84. package/src/service/shutdown/ShutdownState.ts +5 -0
  85. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
  86. package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
  87. package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
  88. package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
  89. package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
  90. package/src/terminal/Ansi.ts +27 -0
  91. package/src/terminal/ImageRenderer.ts +199 -0
  92. package/src/terminal/KeyReader.ts +23 -0
  93. package/src/terminal/PngDecoder.ts +143 -0
  94. package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
  95. package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
  96. package/src/terminal/TtyKeyReader.ts +83 -0
  97. package/src/terminal/TtyStyler.ts +78 -0
  98. package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
  99. package/src/terminal/supportsHyperlinks.ts +110 -0
  100. package/src/util/configHelper.ts +2 -2
  101. package/src/util/envVarHelper.ts +2 -2
  102. package/src/util/helpHelper.ts +139 -123
  103. package/src/util/logger.ts +16 -14
  104. package/src/util/runnerHelper.ts +41 -0
  105. package/tests/cli/BaseCLI.test.ts +50 -54
  106. package/tests/command/CompletionCommand.test.ts +68 -0
  107. package/tests/command/UsageCommand.test.ts +3 -2
  108. package/tests/command/VersionCommand.test.ts +3 -2
  109. package/tests/fixtures/Context.ts +9 -2
  110. package/tests/fixtures/util.ts +1 -1
  111. package/tests/runtime/command/CommandValidator.test.ts +103 -0
  112. package/tests/runtime/parser.test.ts +9 -7
  113. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
  114. package/tests/runtime/runner.test.ts +36 -0
  115. package/tests/runtime/values/argumentValueValidation.test.ts +215 -1
  116. package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
  117. package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
  118. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
  119. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
  120. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
  121. package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
  122. package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
  123. package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
  124. package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
  125. package/tests/service/completion/shell/bash.test.ts +73 -0
  126. package/tests/service/completion/shell/fish.test.ts +62 -0
  127. package/tests/service/completion/shell/powershell.test.ts +59 -0
  128. package/tests/service/completion/shell/zsh.test.ts +60 -0
  129. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
  130. package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
  131. package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
  132. package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
  133. package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
  134. package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
  135. package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
  136. package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
  137. package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
  138. package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
  139. package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
  140. package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
  141. package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
  142. package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
  143. package/tests/service/printer/command/LogLevelCommand.test.ts +8 -6
  144. package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
  145. package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
  146. package/tests/service/printer/terminal/Progress.test.ts +64 -4
  147. package/tests/service/printer/terminal/Spinner.test.ts +44 -3
  148. package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
  149. package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
  150. package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
  151. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
  152. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
  153. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
  154. package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
  155. package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
  156. package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
  157. package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
  158. package/tests/terminal/ImageRenderer.test.ts +161 -0
  159. package/tests/terminal/PngDecoder.test.ts +223 -0
  160. package/tests/terminal/supportsHyperlinks.test.ts +149 -0
  161. package/tests/util/configHelper.test.ts +4 -4
  162. package/tests/util/logger.test.ts +23 -1
  163. package/tsconfig.json +31 -0
  164. package/src/service/printer/terminal/Ansi.ts +0 -10
  165. package/src/service/printer/terminal/TtyStyler.ts +0 -39
@@ -23,19 +23,38 @@ import UsageCommand from "../command/UsageCommand.ts";
23
23
  import {
24
24
  isGlobalCommand,
25
25
  isGlobalModifierCommand,
26
+ isGroupCommand,
26
27
  isSubCommand,
27
28
  } from "../runtime/command/CommandTypeGuards.ts";
28
29
  import CommandValidator from "../runtime/command/CommandValidator.ts";
29
30
  import ShutdownServiceProvider from "../service/shutdown/ShutdownServiceProvider.ts";
31
+ import { shutdownState } from "../service/shutdown/ShutdownState.ts";
30
32
  import ConfigurationServiceProvider from "../service/configuration/ConfigurationServiceProvider.ts";
31
33
  import PrinterServiceProvider from "../service/printer/PrinterServiceProvider.ts";
34
+ import TableGeneratorServiceProvider from "../service/tableGenerator/TableGeneratorServiceProvider.ts";
32
35
  import { run } from "../runtime/runner.ts";
33
36
 
34
37
  import { WritableStream } from "node:stream/web";
35
- import Terminal from "../service/printer/terminal/Terminal.ts";
36
- import PrinterService from "../api/service/core/PrinterService.ts";
38
+ import type Terminal from "../terminal/Terminal.ts";
39
+ import type PrinterService from "../api/service/core/PrinterService.ts";
37
40
  import DefaultPrinterService from "../service/printer/DefaultPrinterService.ts";
38
- import Styler from "../service/printer/terminal/Styler.ts";
41
+ import type Styler from "../terminal/Styler.ts";
42
+ import type KeyReader from "../terminal/KeyReader.ts";
43
+ import type BaseCLIFeatureOptions from "../api/BaseCLIFeatureOptions.ts";
44
+ import DefaultPrompterService, {
45
+ DEFAULT_PROMPTER_CONFIG,
46
+ } from "../service/prompter/DefaultPrompterService.ts";
47
+ import PrompterServiceProvider from "../service/prompter/PrompterServiceProvider.ts";
48
+ import DefaultArgumentPrompterService from "../service/argumentPrompter/DefaultArgumentPrompterService.ts";
49
+ import ArgumentPrompterServiceProvider from "../service/argumentPrompter/ArgumentPrompterServiceProvider.ts";
50
+ import DefaultCompletionService from "../service/completion/DefaultCompletionService.ts";
51
+ import CompletionServiceProvider from "../service/completion/CompletionServiceProvider.ts";
52
+ import ImagePrinterServiceProvider from "../service/imagePrinter/ImagePrinterServiceProvider.ts";
53
+ import {
54
+ CompletionCompleteSubCommand,
55
+ CompletionGroupCommand,
56
+ CompletionIntegrationSubCommand,
57
+ } from "../command/CompletionCommand.ts";
39
58
  const logger = getLogger("BaseCLI");
40
59
 
41
60
  /**
@@ -46,10 +65,15 @@ const logger = getLogger("BaseCLI");
46
65
  * * {@link ShutdownServiceProvider}
47
66
  * * {@link PrinterServiceProvider}
48
67
  * * {@link ConfigurationServiceProvider}
68
+ * * {@link TableGeneratorServiceProvider}
49
69
  *
50
70
  * NOTE: The `ConfigurationServiceProvider` will not be added if the required `allow-read` and `allow-write`
51
71
  * permissions are not granted. It can also be excluded if desired via a constructor argument.
52
72
  *
73
+ * Optionally enabled via {@link BaseCLIFeatureOptions}:
74
+ *
75
+ * * {@link ImagePrinterServiceProvider}
76
+ *
53
77
  * By default the following commands are added:
54
78
  *
55
79
  * * {@link SingleCommandCliHelpSubCommand} or {@link MultiCommandCliHelpSubCommand}
@@ -59,47 +83,32 @@ const logger = getLogger("BaseCLI");
59
83
  */
60
84
  export default class BaseCLI implements CLI {
61
85
  readonly #cliConfig: CLIConfig;
62
- readonly #envVarsEnabled: boolean;
63
- readonly #configEnabled: boolean;
64
- readonly #keyValueServiceEnabled: boolean;
86
+ readonly #options: Required<BaseCLIFeatureOptions>;
87
+ readonly #keyReader: KeyReader;
88
+ readonly #stderrTerminal: Terminal;
65
89
  readonly #addedNonModifierCommands: Array<Command>;
66
90
  readonly #serviceProviderRegistry: DefaultServiceProviderRegistry;
67
91
  readonly #commandRegistry: DefaultCommandRegistry;
68
92
  readonly #context: DefaultContext;
69
93
  readonly #printerService: PrinterService;
94
+ readonly #stdoutTerminal: Terminal;
70
95
 
71
- /**
72
- * Constructor configures the instance with the specified CLI application details and WritableStream instances.
73
- *
74
- * @param cliConfig the {@link CLIConfig} for the CLI application.
75
- * @param stdoutWritableStream the WritableStream to use for stderr output.
76
- * @param stderrWritableStream the WritableStream to use for stderr output.
77
- * @param stdoutIsColor true if stdout supports color.
78
- * @param stderrIsColor true if stderr supports color.
79
- * @param terminal the Terminal implementation to use.
80
- * @param styler the Styler implementation to use.
81
- * @param envVarsEnabled optionally support checking env variables for default argument values.
82
- * @param configEnabled optionally enable configuration file support for default argument values.
83
- * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configEnabled` must be true in this case
84
- * @param validateAllCommands optionally force validation of all i.e. include validation of core commands.
85
- */
86
96
  constructor(
87
97
  cliConfig: CLIConfig,
88
98
  stdoutWritableStream: WritableStream,
89
99
  stderrWritableStream: WritableStream,
90
100
  stdoutIsColor: boolean,
91
101
  stderrIsColor: boolean,
102
+ stdoutTerminal: Terminal,
92
103
  stderrTerminal: Terminal,
93
104
  styler: Styler,
94
- envVarsEnabled = false,
95
- configEnabled = false,
96
- keyValueServiceEnabled = false,
97
- validateAllCommands = false,
105
+ keyReader: KeyReader,
106
+ options?: BaseCLIFeatureOptions,
98
107
  ) {
99
108
  if (cliConfig.name.length === 0) {
100
109
  throw new Error("Invalid empty CLI name provided");
101
110
  }
102
- if (cliConfig.name[0].match(/[0-9\-_]/)) {
111
+ if (cliConfig.name[0]!.match(/[0-9\-_]/)) {
103
112
  throw new Error(
104
113
  `Invalid CLI name starting with a digit or dash provided: '${cliConfig.name}'`,
105
114
  );
@@ -114,14 +123,23 @@ export default class BaseCLI implements CLI {
114
123
  throw new Error("Invalid empty CLI version provided");
115
124
  }
116
125
  this.#cliConfig = cliConfig;
117
- this.#envVarsEnabled = envVarsEnabled;
118
- this.#configEnabled = configEnabled;
119
- this.#keyValueServiceEnabled = keyValueServiceEnabled;
126
+ this.#options = {
127
+ configFileSupportEnabled: false,
128
+ envVarsSupportEnabled: false,
129
+ keyValueServiceEnabled: false,
130
+ secretServiceEnabled: false,
131
+ argumentPrompterServiceEnabled: false,
132
+ completionServiceEnabled: false,
133
+ imagePrinterServiceEnabled: false,
134
+ validateAllCommands: false,
135
+ ...options,
136
+ };
137
+ this.#keyReader = keyReader;
138
+ this.#stderrTerminal = stderrTerminal;
120
139
  this.#addedNonModifierCommands = [];
121
140
 
122
- // create a validator if required
123
141
  let commandValidator: CommandValidator | undefined;
124
- if (validateAllCommands) {
142
+ if (this.#options.validateAllCommands) {
125
143
  commandValidator = new CommandValidator(this.#cliConfig);
126
144
  }
127
145
 
@@ -134,11 +152,13 @@ export default class BaseCLI implements CLI {
134
152
  // create a context
135
153
  this.#context = new DefaultContext(this.#cliConfig);
136
154
 
155
+ this.#stdoutTerminal = stdoutTerminal;
137
156
  this.#printerService = new DefaultPrinterService(
138
157
  stdoutWritableStream,
139
158
  stderrWritableStream,
140
159
  stdoutIsColor,
141
160
  stderrIsColor,
161
+ stdoutTerminal,
142
162
  stderrTerminal,
143
163
  styler,
144
164
  );
@@ -184,11 +204,12 @@ export default class BaseCLI implements CLI {
184
204
 
185
205
  if (
186
206
  (this.#addedNonModifierCommands.length === 1) &&
187
- !isSubCommand(this.#addedNonModifierCommands[0]) &&
188
- !isGlobalCommand(this.#addedNonModifierCommands[0])
207
+ !isSubCommand(this.#addedNonModifierCommands[0]!) &&
208
+ !isGlobalCommand(this.#addedNonModifierCommands[0]!) &&
209
+ !isGroupCommand(this.#addedNonModifierCommands[0]!)
189
210
  ) {
190
211
  throw new Error(
191
- "If only one command is added, if must be a global command or sub-command!",
212
+ "If only one command is added, it must be a global command, sub-command, or group command!",
192
213
  );
193
214
  }
194
215
 
@@ -200,12 +221,45 @@ export default class BaseCLI implements CLI {
200
221
  this.#printerService,
201
222
  ),
202
223
  );
224
+ this.addServiceProvider(new TableGeneratorServiceProvider(70));
225
+
226
+ const prompterService = new DefaultPrompterService(
227
+ DEFAULT_PROMPTER_CONFIG,
228
+ this.#stderrTerminal,
229
+ this.#keyReader,
230
+ this.#printerService,
231
+ );
232
+ this.addServiceProvider(new PrompterServiceProvider(75, prompterService));
233
+
234
+ if (this.#options.argumentPrompterServiceEnabled) {
235
+ const argumentPrompterService = new DefaultArgumentPrompterService(
236
+ prompterService,
237
+ );
238
+ this.addServiceProvider(
239
+ new ArgumentPrompterServiceProvider(65, argumentPrompterService),
240
+ );
241
+ }
242
+
243
+ if (this.#options.imagePrinterServiceEnabled) {
244
+ this.addServiceProvider(
245
+ new ImagePrinterServiceProvider(55, this.#stdoutTerminal),
246
+ );
247
+ }
248
+
249
+ let completionService: DefaultCompletionService | undefined;
250
+ if (this.#options.completionServiceEnabled) {
251
+ completionService = new DefaultCompletionService();
252
+ this.addServiceProvider(
253
+ new CompletionServiceProvider(60, completionService),
254
+ );
255
+ }
203
256
 
204
257
  const configurationServiceProvider = new ConfigurationServiceProvider(
205
258
  90,
206
- this.#envVarsEnabled,
207
- this.#configEnabled,
208
- this.#keyValueServiceEnabled,
259
+ this.#options.envVarsSupportEnabled,
260
+ this.#options.configFileSupportEnabled,
261
+ this.#options.keyValueServiceEnabled,
262
+ this.#options.secretServiceEnabled,
209
263
  );
210
264
  this.addServiceProvider(configurationServiceProvider);
211
265
 
@@ -213,7 +267,7 @@ export default class BaseCLI implements CLI {
213
267
  const serviceProvider of this.#serviceProviderRegistry
214
268
  .getServiceProviders()
215
269
  ) {
216
- const serviceInfo = await serviceProvider.provide(this.#cliConfig);
270
+ const serviceInfo = await serviceProvider.getServiceInfo(this.#cliConfig);
217
271
 
218
272
  if (serviceInfo.service) {
219
273
  this.#context.addServiceInstance(
@@ -235,27 +289,30 @@ export default class BaseCLI implements CLI {
235
289
 
236
290
  try {
237
291
  // setup help commands and default command based on the number of commands explicitly added via {@link addCommand)
238
- if (this.#addedNonModifierCommands.length === 1) {
292
+ if (
293
+ this.#addedNonModifierCommands.length === 1 &&
294
+ !isGroupCommand(this.#addedNonModifierCommands[0]!)
295
+ ) {
239
296
  // a single-command CLI
240
297
  defaultCommand = this.#addedNonModifierCommands[0];
241
298
  helpSubCommand = new SingleCommandCliHelpSubCommand(
242
- this.#envVarsEnabled,
299
+ this.#options.envVarsSupportEnabled,
243
300
  defaultCommand as SubCommand,
244
301
  this.#commandRegistry,
245
302
  );
246
303
  helpGlobalCommand = new SingleCommandCliHelpGlobalCommand(
247
- this.#envVarsEnabled,
304
+ this.#options.envVarsSupportEnabled,
248
305
  defaultCommand as SubCommand,
249
306
  this.#commandRegistry,
250
307
  );
251
308
  } else {
252
309
  // a multi-command CLI
253
310
  helpSubCommand = new MultiCommandCliHelpSubCommand(
254
- this.#envVarsEnabled,
311
+ this.#options.envVarsSupportEnabled,
255
312
  this.#commandRegistry,
256
313
  );
257
314
  helpGlobalCommand = new MultiCommandCliHelpGlobalCommand(
258
- this.#envVarsEnabled,
315
+ this.#options.envVarsSupportEnabled,
259
316
  this.#commandRegistry,
260
317
  );
261
318
  }
@@ -267,6 +324,16 @@ export default class BaseCLI implements CLI {
267
324
  // register the version command
268
325
  this.#commandRegistry.addCommand(new VersionCommand());
269
326
 
327
+ // register completion commands (need CommandRegistry reference like help commands)
328
+ if (completionService) {
329
+ completionService.setCommandRegistry(this.#commandRegistry);
330
+ const integrationCommand = new CompletionIntegrationSubCommand();
331
+ const completeCommand = new CompletionCompleteSubCommand();
332
+ this.#commandRegistry.addCommand(
333
+ new CompletionGroupCommand(integrationCommand, completeCommand),
334
+ );
335
+ }
336
+
270
337
  // run...
271
338
  const runResult = await run(
272
339
  args,
@@ -299,7 +366,12 @@ export default class BaseCLI implements CLI {
299
366
  }
300
367
  return runResult;
301
368
  } catch (error) {
302
- // An unexpected error in the framework
369
+ if (
370
+ shutdownState.shutdownRequested ||
371
+ (error as Error).message === "Interrupted"
372
+ ) {
373
+ return { runState: RunState.INTERRUPTED };
374
+ }
303
375
  logger.error("Runtime error: %s", (error as Error).message);
304
376
  return {
305
377
  runState: RunState.RUNTIME_ERROR,
@@ -3,25 +3,21 @@ import BaseCLI from "./BaseCLI.ts";
3
3
  import type RunResult from "../api/RunResult.ts";
4
4
  import { RunState } from "../api/RunResult.ts";
5
5
  import type CLIConfig from "../api/CLIConfig.ts";
6
+ import type BaseCLIFeatureOptions from "../api/BaseCLIFeatureOptions.ts";
6
7
  import { Writable } from "node:stream";
7
- import TtyTerminal from "../service/printer/terminal/TtyTerminal.ts";
8
- import TtyStyler from "../service/printer/terminal/TtyStyler.ts";
8
+ import TtyTerminal from "../terminal/TtyTerminal.ts";
9
+ import TtyKeyReader from "../terminal/TtyKeyReader.ts";
10
+ import TtyStyler from "../terminal/TtyStyler.ts";
9
11
  import supportsColor from "supports-color";
12
+ import supportsHyperlinks from "../terminal/supportsHyperlinks.ts";
10
13
 
11
14
  /**
12
15
  * Default Bun implementation of a {@link CLI} using `process.stdout`, `process.stderr` and `process.argv`.
13
16
  */
14
17
  export default class DefaultRuntimeCLI extends BaseCLI {
15
- /**
16
- * Constructor configures the instance with the specified CLI application details
17
- * and making use of `process.stdout` and `process.stderr`.
18
- */
19
18
  constructor(
20
19
  cliConfig: CLIConfig,
21
- envVarsEnabled = false,
22
- configEnabled = false,
23
- keyValueServiceEnabled = false,
24
- validateAllCommands = false,
20
+ options?: BaseCLIFeatureOptions,
25
21
  ) {
26
22
  super(
27
23
  cliConfig,
@@ -29,21 +25,17 @@ export default class DefaultRuntimeCLI extends BaseCLI {
29
25
  Writable.toWeb(process.stderr),
30
26
  supportsColor.stdout !== false,
31
27
  supportsColor.stderr !== false,
28
+ new TtyTerminal(process.stdout),
32
29
  new TtyTerminal(process.stderr),
33
30
  new TtyStyler(
34
31
  supportsColor.stderr === false ? 1 : supportsColor.stderr.level,
32
+ supportsHyperlinks(process.stderr),
35
33
  ),
36
- envVarsEnabled,
37
- configEnabled,
38
- keyValueServiceEnabled,
39
- validateAllCommands,
34
+ new TtyKeyReader(process.stdin),
35
+ options,
40
36
  );
41
37
  }
42
38
 
43
- /**
44
- * Run the CLI using `process.argv` for the arguments and call `process.exit()` passing the
45
- * {@link RunState} value resulting from the invocation.
46
- */
47
39
  override async run(): Promise<RunResult> {
48
40
  const runResult = await super.run(process.argv.slice(2));
49
41
  if (runResult.runState === RunState.RUNTIME_ERROR) {
@@ -0,0 +1,179 @@
1
+ import type GroupCommand from "../api/command/GroupCommand.ts";
2
+ import type SubCommand from "../api/command/SubCommand.ts";
3
+ import type Option from "../api/argument/Option.ts";
4
+ import type Positional from "../api/argument/Positional.ts";
5
+ import type Context from "../api/Context.ts";
6
+ import {
7
+ type ArgumentValues,
8
+ ArgumentValueTypeName,
9
+ } from "../api/argument/ArgumentValueTypes.ts";
10
+ import {
11
+ COMPLETION_SERVICE_ID,
12
+ ShellType,
13
+ } from "../api/service/core/CompletionService.ts";
14
+ import type DefaultCompletionService from "../service/completion/DefaultCompletionService.ts";
15
+ import {
16
+ Icon,
17
+ PRINTER_SERVICE_ID,
18
+ } from "../api/service/core/PrinterService.ts";
19
+ import type PrinterService from "../api/service/core/PrinterService.ts";
20
+
21
+ export class CompletionIntegrationSubCommand implements SubCommand {
22
+ readonly name = "integration";
23
+ readonly description = "Install shell completion integration";
24
+ readonly enableConfiguration = false;
25
+
26
+ readonly options: ReadonlyArray<Option> = [
27
+ {
28
+ name: "config-path",
29
+ type: ArgumentValueTypeName.STRING,
30
+ isOptional: true,
31
+ description: "Path to shell configuration file",
32
+ },
33
+ ];
34
+
35
+ readonly positionals: ReadonlyArray<Positional> = [
36
+ {
37
+ name: "shell",
38
+ type: ArgumentValueTypeName.STRING,
39
+ allowableValues: Object.values(ShellType),
40
+ description: "Shell type to configure",
41
+ },
42
+ ];
43
+
44
+ async execute(
45
+ context: Context,
46
+ argumentValues: ArgumentValues,
47
+ ): Promise<void> {
48
+ const completionService = context.getServiceById(
49
+ COMPLETION_SERVICE_ID,
50
+ ) as DefaultCompletionService;
51
+ const printerService = context.getServiceById(
52
+ PRINTER_SERVICE_ID,
53
+ ) as PrinterService;
54
+
55
+ const shellType = argumentValues.shell as ShellType;
56
+ const configPathArg = argumentValues["config-path"] as string | undefined;
57
+
58
+ const isValid = await completionService.validateShellEnvironment(shellType);
59
+ if (!isValid) {
60
+ await printerService.warn(
61
+ `Shell '${shellType}' was not detected in the current environment. Proceeding anyway.\n`,
62
+ Icon.ALERT,
63
+ );
64
+ }
65
+
66
+ const configPath = configPathArg ||
67
+ completionService.getDefaultConfigPath(shellType);
68
+ const cliName = context.cliConfig.name;
69
+ const bootstrapScript = completionService.getBootstrapScript(
70
+ shellType,
71
+ cliName,
72
+ );
73
+
74
+ const beginMarker = `# BEGIN ${cliName} completion`;
75
+ const endMarker = `# END ${cliName} completion`;
76
+ const block = `${beginMarker}\n${bootstrapScript}\n${endMarker}`;
77
+
78
+ let existingContent = "";
79
+ try {
80
+ existingContent = await Bun.file(configPath).text();
81
+ } catch {
82
+ // File doesn't exist yet
83
+ }
84
+
85
+ let newContent: string;
86
+ const beginIdx = existingContent.indexOf(beginMarker);
87
+ const endIdx = existingContent.indexOf(endMarker);
88
+ if (beginIdx !== -1 && endIdx !== -1) {
89
+ newContent = existingContent.substring(0, beginIdx) +
90
+ block +
91
+ existingContent.substring(endIdx + endMarker.length);
92
+ } else {
93
+ const separator = existingContent.length > 0 &&
94
+ !existingContent.endsWith("\n")
95
+ ? "\n\n"
96
+ : existingContent.length > 0
97
+ ? "\n"
98
+ : "";
99
+ newContent = existingContent + separator + block + "\n";
100
+ }
101
+
102
+ await Bun.write(configPath, newContent);
103
+
104
+ await printerService.info(
105
+ `Shell completion installed for ${shellType}. Restart your shell or run 'source ${configPath}' to activate.\n`,
106
+ );
107
+ }
108
+ }
109
+
110
+ export class CompletionCompleteSubCommand implements SubCommand {
111
+ readonly name = "complete";
112
+ readonly description = "Generate completions for shell integration";
113
+ readonly enableConfiguration = false;
114
+
115
+ readonly options: ReadonlyArray<Option> = [];
116
+
117
+ readonly positionals: ReadonlyArray<Positional> = [
118
+ {
119
+ name: "shell",
120
+ type: ArgumentValueTypeName.STRING,
121
+ allowableValues: Object.values(ShellType),
122
+ description: "Shell type",
123
+ },
124
+ {
125
+ name: "args",
126
+ type: ArgumentValueTypeName.STRING,
127
+ isVarargMultiple: true,
128
+ isVarargOptional: true,
129
+ description: "Shell-specific completion context arguments",
130
+ },
131
+ ];
132
+
133
+ async execute(
134
+ context: Context,
135
+ argumentValues: ArgumentValues,
136
+ ): Promise<void> {
137
+ const completionService = context.getServiceById(
138
+ COMPLETION_SERVICE_ID,
139
+ ) as DefaultCompletionService;
140
+ const printerService = context.getServiceById(
141
+ PRINTER_SERVICE_ID,
142
+ ) as PrinterService;
143
+
144
+ const shellType = argumentValues.shell as ShellType;
145
+ const args = (argumentValues.args || []) as string[];
146
+
147
+ const completionContext = completionService.parseCompletionContext(
148
+ shellType,
149
+ args,
150
+ );
151
+ const completions = await completionService.generateCompletions(
152
+ shellType,
153
+ completionContext.line,
154
+ completionContext.cursorPosition,
155
+ );
156
+
157
+ const output = completionService.formatCompletions(shellType, completions);
158
+ if (output.length > 0) {
159
+ await printerService.print(output + "\n");
160
+ }
161
+ }
162
+ }
163
+
164
+ export class CompletionGroupCommand implements GroupCommand {
165
+ readonly name = "completions";
166
+ readonly description = "Manage shell auto-completion";
167
+ readonly enableConfiguration = false;
168
+ readonly memberSubCommands: ReadonlyArray<SubCommand>;
169
+
170
+ constructor(
171
+ integrationCommand: CompletionIntegrationSubCommand,
172
+ completeCommand: CompletionCompleteSubCommand,
173
+ ) {
174
+ this.memberSubCommands = [integrationCommand, completeCommand];
175
+ }
176
+
177
+ async execute(_context: Context): Promise<void> {
178
+ }
179
+ }
@@ -3,7 +3,6 @@ import type Option from "../api/argument/Option.ts";
3
3
  import type SubCommand from "../api/command/SubCommand.ts";
4
4
  import type GlobalCommand from "../api/command/GlobalCommand.ts";
5
5
  import type GroupCommand from "../api/command/GroupCommand.ts";
6
- import { distance } from "fastest-levenshtein";
7
6
  import {
8
7
  type ArgumentSingleValueType,
9
8
  type ArgumentValues,
@@ -12,6 +11,7 @@ import {
12
11
  import type Context from "../api/Context.ts";
13
12
  import type { HelpSection } from "../util/helpHelper.ts";
14
13
  import {
14
+ findPossibleCommandNames,
15
15
  getCommandArgsHelpSections,
16
16
  getCommandExamplesHelpSections,
17
17
  getGlobalArgumentHelpEntry,
@@ -26,6 +26,8 @@ import {
26
26
  Icon,
27
27
  PRINTER_SERVICE_ID,
28
28
  } from "../api/service/core/PrinterService.ts";
29
+ import type TableGeneratorService from "../api/service/core/TableGeneratorService.ts";
30
+ import { TABLE_GENERATOR_SERVICE_ID } from "../api/service/core/TableGeneratorService.ts";
29
31
 
30
32
  /**
31
33
  * Provides common implementation for both {@link MultiCommandCliHelpGlobalCommand} and {@link MultiCommandCliHelpSubCommand}.
@@ -44,34 +46,6 @@ abstract class MultiCommandCliAbstractHelpCommand {
44
46
  this.#commandRegistry = commandRegistry;
45
47
  }
46
48
 
47
- #findPossibleCommandNames(
48
- commandName: string,
49
- groupCommands: ReadonlyArray<GroupCommand>,
50
- subCommands: ReadonlyArray<SubCommand>,
51
- ): string[] {
52
- const levenCommandArray = new Array<[number, string]>();
53
- subCommands.forEach((subCommand) => {
54
- levenCommandArray.push([
55
- distance(subCommand.name, commandName),
56
- subCommand.name,
57
- ]);
58
- });
59
- groupCommands.forEach((groupCommand) => {
60
- groupCommand.memberSubCommands.forEach((memberCommand) => {
61
- const memberName = `${groupCommand.name}:${memberCommand.name}`;
62
- levenCommandArray.push([
63
- distance(memberName, commandName),
64
- memberName,
65
- ]);
66
- });
67
- });
68
- return levenCommandArray
69
- .sort((a, b) => a[0] - b[0])
70
- .slice(0, 2)
71
- .filter((value) => value[0] < 3)
72
- .map((value) => value[1]);
73
- }
74
-
75
49
  #getGlobalCommandsHelpSection(
76
50
  context: Context,
77
51
  title: string,
@@ -206,6 +180,9 @@ abstract class MultiCommandCliAbstractHelpCommand {
206
180
  const printerService = context.getServiceById(
207
181
  PRINTER_SERVICE_ID,
208
182
  ) as PrinterService;
183
+ const tableGeneratorService = context.getServiceById(
184
+ TABLE_GENERATOR_SERVICE_ID,
185
+ ) as TableGeneratorService;
209
186
 
210
187
  const globalModifierCommands = this.#commandRegistry
211
188
  .getGlobalModifierCommands();
@@ -238,7 +215,11 @@ abstract class MultiCommandCliAbstractHelpCommand {
238
215
  ),
239
216
  );
240
217
 
241
- await printHelpSections(printerService, helpSections);
218
+ await printHelpSections(
219
+ printerService,
220
+ tableGeneratorService,
221
+ helpSections,
222
+ );
242
223
  }
243
224
 
244
225
  protected async printUsageHelp(
@@ -248,6 +229,9 @@ abstract class MultiCommandCliAbstractHelpCommand {
248
229
  const printerService = context.getServiceById(
249
230
  PRINTER_SERVICE_ID,
250
231
  ) as PrinterService;
232
+ const tableGeneratorService = context.getServiceById(
233
+ TABLE_GENERATOR_SERVICE_ID,
234
+ ) as TableGeneratorService;
251
235
 
252
236
  const groupCommands = this.#commandRegistry.getGroupCommands();
253
237
  const subCommands = this.#commandRegistry.getSubCommands();
@@ -273,7 +257,7 @@ abstract class MultiCommandCliAbstractHelpCommand {
273
257
  );
274
258
 
275
259
  // look for other possible matches
276
- const possibleCommandNames = this.#findPossibleCommandNames(
260
+ const possibleCommandNames = findPossibleCommandNames(
277
261
  commandName,
278
262
  groupCommands,
279
263
  subCommands,
@@ -328,7 +312,11 @@ abstract class MultiCommandCliAbstractHelpCommand {
328
312
  ...getCommandExamplesHelpSections(printerService, context, subCommand),
329
313
  );
330
314
 
331
- await printHelpSections(printerService, helpSections);
315
+ await printHelpSections(
316
+ printerService,
317
+ tableGeneratorService,
318
+ helpSections,
319
+ );
332
320
  }
333
321
  }
334
322
 
@@ -17,6 +17,8 @@ import type GroupCommand from "../api/command/GroupCommand.ts";
17
17
  import type CommandRegistry from "../runtime/registry/CommandRegistry.ts";
18
18
  import type PrinterService from "../api/service/core/PrinterService.ts";
19
19
  import { PRINTER_SERVICE_ID } from "../api/service/core/PrinterService.ts";
20
+ import type TableGeneratorService from "../api/service/core/TableGeneratorService.ts";
21
+ import { TABLE_GENERATOR_SERVICE_ID } from "../api/service/core/TableGeneratorService.ts";
20
22
 
21
23
  /**
22
24
  * Provides common implementation for both {@link SingleCommandCliHelpGlobalCommand} and {@link SingleCommandCliHelpSubCommand}.
@@ -149,6 +151,9 @@ abstract class SingleCommandCliAbstractHelpCommand {
149
151
  const printerService = context.getServiceById(
150
152
  PRINTER_SERVICE_ID,
151
153
  ) as PrinterService;
154
+ const tableGeneratorService = context.getServiceById(
155
+ TABLE_GENERATOR_SERVICE_ID,
156
+ ) as TableGeneratorService;
152
157
 
153
158
  helpSections.push(
154
159
  ...getCommandExamplesHelpSections(
@@ -158,7 +163,11 @@ abstract class SingleCommandCliAbstractHelpCommand {
158
163
  ),
159
164
  );
160
165
 
161
- await printHelpSections(printerService, helpSections);
166
+ await printHelpSections(
167
+ printerService,
168
+ tableGeneratorService,
169
+ helpSections,
170
+ );
162
171
  }
163
172
  }
164
173