@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
package/src/launcher.ts CHANGED
@@ -1,33 +1,20 @@
1
1
  import process from "node:process";
2
2
  import path from "node:path";
3
3
  import type CLIConfig from "./api/CLIConfig.ts";
4
+ import type BaseCLIFeatureOptions from "./api/BaseCLIFeatureOptions.ts";
4
5
  import type SubCommand from "./api/command/SubCommand.ts";
5
6
  import DefaultRuntimeCLI from "./cli/DefaultRuntimeCLI.ts";
6
7
  import type Command from "./api/command/Command.ts";
7
8
  import type RunResult from "./api/RunResult.ts";
8
9
  import type { ServiceProvider } from "./api/service/ServiceProvider.ts";
9
10
 
10
- /**
11
- * Launch a {@link DefaultRuntimeCLI} with a single specified {@link SubCommand} instance.
12
- *
13
- * @param command the {@link SubCommand} to add to the CLI.
14
- * @param description optional description of the CLI.
15
- * @param name optional name of the CLI. If not provided, `process.execPath` will be used in an attempt to derive the name.
16
- * @param version optional version of the CLI.
17
- * @param envVarsEnabled optionally support checking env variables for default argument values.
18
- * @param configEnabled optionally enable configuration file support for default argument values.
19
- * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configEnabled` must be true in this case
20
- * @param serviceProviders optional array of {@link ServiceProvider} instances to add to the CLI.
21
- */
22
11
  export async function launchSingleCommandCLI(
23
12
  command: SubCommand,
24
13
  description?: string,
25
14
  name?: string,
26
15
  version?: string,
27
- envVarsEnabled = false,
28
- configEnabled = false,
29
- keyValueServiceEnabled = false,
30
16
  serviceProviders?: ReadonlyArray<ServiceProvider>,
17
+ options?: BaseCLIFeatureOptions,
31
18
  ): Promise<RunResult> {
32
19
  if (!name) {
33
20
  name = path.basename(process.execPath);
@@ -38,20 +25,12 @@ export async function launchSingleCommandCLI(
38
25
  version: version || "N/A",
39
26
  };
40
27
 
41
- if (!configEnabled && keyValueServiceEnabled) {
42
- throw new Error(
43
- "configEnabled must be true if keyValueServiceEnabled is true",
44
- );
45
- }
46
- const validateAllCommands =
47
- process.env.DYNAMIC_CLI_FRAMEWORK_VALIDATE_ALL !== undefined;
48
- const cli = new DefaultRuntimeCLI(
49
- cliConfig,
50
- envVarsEnabled,
51
- configEnabled,
52
- keyValueServiceEnabled,
53
- validateAllCommands,
54
- );
28
+ const mergedOptions: BaseCLIFeatureOptions = {
29
+ ...options,
30
+ validateAllCommands: (options?.validateAllCommands ?? false) ||
31
+ process.env.DYNAMIC_CLI_FRAMEWORK_VALIDATE_ALL !== undefined,
32
+ };
33
+ const cli = new DefaultRuntimeCLI(cliConfig, mergedOptions);
55
34
 
56
35
  cli.addCommand(command);
57
36
 
@@ -60,27 +39,13 @@ export async function launchSingleCommandCLI(
60
39
  return await cli.run();
61
40
  }
62
41
 
63
- /**
64
- * Launch a {@link DefaultRuntimeCLI} with multiple specified {@link Command} instances.
65
- *
66
- * @param commands the {@link Command} instances to add to the CLI.
67
- * @param description optional description of the CLI.
68
- * @param name optional name of the CLI. If not provided, `process.execPath` will be used in an attempt to derive the name.
69
- * @param version optional version of the CLI.
70
- * @param envVarsEnabled optionally support checking env variables for default argument values.
71
- * @param configEnabled optionally enable configuration file support for default argument values.
72
- * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configEnabled` must be true in this case
73
- * @param serviceProviders optional array of {@link ServiceProvider} instances to add to the CLI.
74
- */
75
42
  export async function launchMultiCommandCLI(
76
43
  commands: ReadonlyArray<Command>,
77
44
  description?: string,
78
45
  name?: string,
79
46
  version?: string,
80
- envVarsEnabled = false,
81
- configEnabled = false,
82
- keyValueServiceEnabled = false,
83
47
  serviceProviders?: ReadonlyArray<ServiceProvider>,
48
+ options?: BaseCLIFeatureOptions,
84
49
  ): Promise<RunResult> {
85
50
  if (!name) {
86
51
  name = path.basename(process.execPath);
@@ -90,20 +55,13 @@ export async function launchMultiCommandCLI(
90
55
  name,
91
56
  version: version || "N/A",
92
57
  };
93
- if (!configEnabled && keyValueServiceEnabled) {
94
- throw new Error(
95
- "configEnabled must be true if keyValueServiceEnabled is true",
96
- );
97
- }
98
- const validateAllCommands =
99
- process.env.DYNAMIC_CLI_FRAMEWORK_VALIDATE_ALL !== undefined;
100
- const cli = new DefaultRuntimeCLI(
101
- cliConfig,
102
- envVarsEnabled,
103
- configEnabled,
104
- keyValueServiceEnabled,
105
- validateAllCommands,
106
- );
58
+
59
+ const mergedOptions: BaseCLIFeatureOptions = {
60
+ ...options,
61
+ validateAllCommands: (options?.validateAllCommands ?? false) ||
62
+ process.env.DYNAMIC_CLI_FRAMEWORK_VALIDATE_ALL !== undefined,
63
+ };
64
+ const cli = new DefaultRuntimeCLI(cliConfig, mergedOptions);
107
65
 
108
66
  commands.forEach((command) => cli.addCommand(command));
109
67
 
@@ -98,7 +98,7 @@ function isNameLegal(name: string): boolean {
98
98
  function isConfigurationKeyLegal(configurationKey: string): boolean {
99
99
  return (configurationKey.length > 0) &&
100
100
  isUppercaseAlphaNumericOrUnderscore(configurationKey) &&
101
- !configurationKey[0].match(/[0-9]/);
101
+ !configurationKey[0]!.match(/[0-9]/);
102
102
  }
103
103
 
104
104
  function validateArgument(argument: Argument, name: string): void {
@@ -368,7 +368,7 @@ export default class CommandValidator {
368
368
  }
369
369
  if (subCommand.positionals) {
370
370
  for (let i = 0; i < subCommand.positionals.length; i += 1) {
371
- const positional = subCommand.positionals[i];
371
+ const positional = subCommand.positionals[i]!;
372
372
  validateSubCommandArgument(positional);
373
373
  if (argumentNames.includes(positional.name)) {
374
374
  throw new Error(
@@ -396,7 +396,7 @@ export default class CommandValidator {
396
396
  command: Command,
397
397
  argumentAncestry: Array<SubCommandArgument>,
398
398
  ) {
399
- const argument = argumentAncestry[argumentAncestry.length - 1];
399
+ const argument = argumentAncestry[argumentAncestry.length - 1]!;
400
400
  if (command.enableConfiguration === true) {
401
401
  if (
402
402
  (argument.configurationKey !== undefined) &&
@@ -33,9 +33,35 @@ import type GroupCommand from "../api/command/GroupCommand.ts";
33
33
  import type SubCommand from "../api/command/SubCommand.ts";
34
34
  import type GlobalCommand from "../api/command/GlobalCommand.ts";
35
35
  import type Command from "../api/command/Command.ts";
36
+ import type ArgumentPrompterService from "../api/service/core/ArgumentPrompterService.ts";
37
+ import { ARGUMENT_PROMPTER_SERVICE_ID } from "../api/service/core/ArgumentPrompterService.ts";
36
38
 
37
39
  const logger = getLogger("runner");
38
40
 
41
+ async function attemptPromptForMissingArguments(
42
+ parseResult: ParseResult,
43
+ context: Context,
44
+ ): Promise<ParseResult | undefined> {
45
+ if (!context.doesServiceExist(ARGUMENT_PROMPTER_SERVICE_ID)) {
46
+ return undefined;
47
+ }
48
+ const service = context.getServiceById(
49
+ ARGUMENT_PROMPTER_SERVICE_ID,
50
+ ) as ArgumentPrompterService;
51
+ try {
52
+ const updated = await service.promptForMissingArguments(parseResult);
53
+ if (updated.invalidArguments.length === 0) {
54
+ return updated;
55
+ }
56
+ } catch (error) {
57
+ if ((error as Error).message === "Interrupted") {
58
+ throw error;
59
+ }
60
+ return undefined;
61
+ }
62
+ return undefined;
63
+ }
64
+
39
65
  /**
40
66
  * Execute the {@link Command} with arguments contained in the provided {@link ParseResult}.
41
67
  *
@@ -56,13 +82,19 @@ async function executeParsedCommand(
56
82
  "Executing group command with name: %s",
57
83
  parseResult!.groupCommand!.name,
58
84
  );
59
- if (configurationServiceProvider?.keyValueServiceEnabled) {
85
+ if (
86
+ configurationServiceProvider?.keyValueServiceEnabled ||
87
+ configurationServiceProvider?.secretServiceEnabled
88
+ ) {
60
89
  configurationServiceProvider.setCommandKeyValueScope(
61
90
  parseResult!.groupCommand!.name,
62
91
  );
63
92
  }
64
93
  await parseResult.groupCommand.execute(context);
65
- if (configurationServiceProvider?.keyValueServiceEnabled) {
94
+ if (
95
+ configurationServiceProvider?.keyValueServiceEnabled ||
96
+ configurationServiceProvider?.secretServiceEnabled
97
+ ) {
66
98
  await configurationServiceProvider.clearKeyValueScope();
67
99
  }
68
100
  }
@@ -73,7 +105,10 @@ async function executeParsedCommand(
73
105
  parseResult!.populatedArgumentValues,
74
106
  );
75
107
 
76
- if (configurationServiceProvider?.keyValueServiceEnabled) {
108
+ if (
109
+ configurationServiceProvider?.keyValueServiceEnabled ||
110
+ configurationServiceProvider?.secretServiceEnabled
111
+ ) {
77
112
  configurationServiceProvider.setCommandKeyValueScope(
78
113
  parseResult.command.name,
79
114
  );
@@ -89,10 +124,16 @@ async function executeParsedCommand(
89
124
  parseResult.populatedArgumentValues as ArgumentSingleValueType,
90
125
  );
91
126
  }
92
- if (configurationServiceProvider?.keyValueServiceEnabled) {
127
+ if (
128
+ configurationServiceProvider?.keyValueServiceEnabled ||
129
+ configurationServiceProvider?.secretServiceEnabled
130
+ ) {
93
131
  await configurationServiceProvider.clearKeyValueScope();
94
132
  }
95
133
  } catch (err) {
134
+ if ((err as Error).message === "Interrupted") {
135
+ throw err;
136
+ }
96
137
  await printCommandExecutionError(
97
138
  context,
98
139
  parseResult,
@@ -155,7 +196,7 @@ async function findAndExecuteGlobalModifierCommands(
155
196
  ) {
156
197
  // parse and fast fail on error
157
198
  const defaultArgumentValues = configurationServiceProvider
158
- ? configurationServiceProvider
199
+ ? await configurationServiceProvider
159
200
  .getDefaultArgumentValues(
160
201
  context.cliConfig,
161
202
  globalModifierCommandClause.command,
@@ -195,7 +236,7 @@ async function findAndExecuteGlobalModifierCommands(
195
236
  for (const globalModifierCommand of remainingGlobalModifierCommands) {
196
237
  // if there is a config entry run this by default even though no arguments were provided on the command line
197
238
  const defaultArgumentValues = configurationServiceProvider
198
- ? configurationServiceProvider
239
+ ? await configurationServiceProvider
199
240
  .getDefaultArgumentValues(context.cliConfig, globalModifierCommand)
200
241
  : undefined;
201
242
 
@@ -274,7 +315,7 @@ async function findAndExecuteNonModifierCommand(
274
315
 
275
316
  if (scanResult.nonModifierCommandClause) {
276
317
  const defaultArgumentValues = configurationServiceProvider
277
- ? configurationServiceProvider
318
+ ? await configurationServiceProvider
278
319
  .getDefaultArgumentValues(
279
320
  context.cliConfig,
280
321
  scanResult.nonModifierCommandClause.command,
@@ -294,12 +335,20 @@ async function findAndExecuteNonModifierCommand(
294
335
  }
295
336
 
296
337
  if (parseResult.invalidArguments.length > 0) {
297
- await printParseResultError(context, parseResult);
298
- return {
299
- runState: RunState.PARSE_ERROR,
300
- command: parseResult.command,
301
- invalidArguments: parseResult.invalidArguments,
302
- };
338
+ const prompted = await attemptPromptForMissingArguments(
339
+ parseResult,
340
+ context,
341
+ );
342
+ if (prompted) {
343
+ parseResult = prompted;
344
+ } else {
345
+ await printParseResultError(context, parseResult);
346
+ return {
347
+ runState: RunState.PARSE_ERROR,
348
+ command: parseResult.command,
349
+ invalidArguments: parseResult.invalidArguments,
350
+ };
351
+ }
303
352
  }
304
353
 
305
354
  // add any unused args back to the list of still unused args
@@ -311,7 +360,7 @@ async function findAndExecuteNonModifierCommand(
311
360
  for (const nonModifierCommand of nonModifierCommandsByName.values()) {
312
361
  // if there is a config entry run this by default even though no arguments were provided on the command line
313
362
  const defaultArgumentValues = configurationServiceProvider
314
- ? configurationServiceProvider
363
+ ? await configurationServiceProvider
315
364
  .getDefaultArgumentValues(context.cliConfig, nonModifierCommand)
316
365
  : undefined;
317
366
 
@@ -329,12 +378,20 @@ async function findAndExecuteNonModifierCommand(
329
378
  }
330
379
 
331
380
  if (parseResult.invalidArguments.length > 0) {
332
- await printParseResultError(context, parseResult);
333
- return {
334
- runState: RunState.PARSE_ERROR,
335
- command: parseResult.command,
336
- invalidArguments: parseResult.invalidArguments,
337
- };
381
+ const prompted = await attemptPromptForMissingArguments(
382
+ parseResult,
383
+ context,
384
+ );
385
+ if (prompted) {
386
+ parseResult = prompted;
387
+ } else {
388
+ await printParseResultError(context, parseResult);
389
+ return {
390
+ runState: RunState.PARSE_ERROR,
391
+ command: parseResult.command,
392
+ invalidArguments: parseResult.invalidArguments,
393
+ };
394
+ }
338
395
  }
339
396
 
340
397
  break;
@@ -376,7 +433,7 @@ async function findAndExecuteDefaultNonModifierCommand(
376
433
  context: Context,
377
434
  ): Promise<RunResult | undefined> {
378
435
  const defaultArgumentValues = configurationServiceProvider
379
- ? configurationServiceProvider
436
+ ? await configurationServiceProvider
380
437
  .getDefaultArgumentValues(context.cliConfig, defaultNonModifierCommand)
381
438
  : undefined;
382
439
  const isGlobal = isGlobalCommand(defaultNonModifierCommand);
@@ -448,14 +505,23 @@ async function findAndExecuteDefaultNonModifierCommand(
448
505
  }
449
506
  }
450
507
 
451
- // if there were invalid arguments, treat this as the error
508
+ // if there were invalid arguments, attempt prompting before treating as error
452
509
  if (lastErroneousParseResult) {
453
- await printParseResultError(context, lastErroneousParseResult, true);
454
- return {
455
- runState: RunState.PARSE_ERROR,
456
- command: defaultNonModifierCommand,
457
- invalidArguments: lastErroneousParseResult.invalidArguments,
458
- };
510
+ const prompted = await attemptPromptForMissingArguments(
511
+ lastErroneousParseResult,
512
+ context,
513
+ );
514
+ if (prompted) {
515
+ parseResult = prompted;
516
+ lastErroneousParseResult = undefined;
517
+ } else {
518
+ await printParseResultError(context, lastErroneousParseResult, true);
519
+ return {
520
+ runState: RunState.PARSE_ERROR,
521
+ command: defaultNonModifierCommand,
522
+ invalidArguments: lastErroneousParseResult.invalidArguments,
523
+ };
524
+ }
459
525
  }
460
526
 
461
527
  // give up if still nothing found
@@ -604,14 +670,20 @@ export async function run(
604
670
 
605
671
  logger.debug("Initialising service with ID: %s", serviceProvider.serviceId);
606
672
 
607
- if (configurationServiceProvider?.keyValueServiceEnabled) {
673
+ if (
674
+ configurationServiceProvider?.keyValueServiceEnabled ||
675
+ configurationServiceProvider?.secretServiceEnabled
676
+ ) {
608
677
  configurationServiceProvider.setServiceKeyValueScope(
609
678
  serviceProvider.serviceId,
610
679
  );
611
680
  }
612
681
 
613
682
  await serviceProvider.initService(context);
614
- if (configurationServiceProvider?.keyValueServiceEnabled) {
683
+ if (
684
+ configurationServiceProvider?.keyValueServiceEnabled ||
685
+ configurationServiceProvider?.secretServiceEnabled
686
+ ) {
615
687
  await configurationServiceProvider.clearKeyValueScope();
616
688
  }
617
689
  }
@@ -67,15 +67,15 @@ function normaliseFirstArgumentIfRequired(
67
67
  // --<global_[modifier_]command_name>=<value>
68
68
  // -<global_[modifier_]command_short_alias>
69
69
  // -<global_[modifier_]command_short_alias>=<value>
70
- if ((firstArg.length < 2) || (firstArg.charAt(0) !== "-")) {
70
+ if ((firstArg!.length < 2) || (firstArg!.charAt(0) !== "-")) {
71
71
  return args;
72
72
  }
73
73
 
74
74
  // looking for:
75
75
  // --<global_[modifier_]command_name>
76
76
  // --<global_[modifier_]command_name>=<value>
77
- if ((firstArg.length > 2) && (firstArg.charAt(1) === "-")) {
78
- let potentialGlobalCommandName = firstArg.slice(2);
77
+ if ((firstArg!.length > 2) && (firstArg!.charAt(1) === "-")) {
78
+ let potentialGlobalCommandName = firstArg!.slice(2);
79
79
 
80
80
  // looking for:
81
81
  // <global_command_name>=<value>
@@ -84,7 +84,7 @@ function normaliseFirstArgumentIfRequired(
84
84
  if (potentialGlobalCommandName.includes("=")) {
85
85
  [potentialGlobalCommandName, nextArg] = potentialGlobalCommandName.split(
86
86
  /=(.*)/,
87
- );
87
+ ) as [string, string | undefined];
88
88
  }
89
89
 
90
90
  if (!globalCommandsByName.has(potentialGlobalCommandName)) {
@@ -105,14 +105,17 @@ function normaliseFirstArgumentIfRequired(
105
105
  // -<global_[modifier_]command_short_alias>
106
106
  // -<global_[modifier_]command_short_alias>=<value>
107
107
  // (by now we know the arg starts with -<char>)
108
- let potentialGlobalCommandShortAlias = firstArg.slice(1);
108
+ let potentialGlobalCommandShortAlias = firstArg!.slice(1);
109
109
 
110
110
  // looking for:
111
111
  // <global_[modifier_]command_short_alias>=<value>
112
112
  let nextArg: string | undefined;
113
113
  if (potentialGlobalCommandShortAlias.includes("=")) {
114
114
  [potentialGlobalCommandShortAlias, nextArg] =
115
- potentialGlobalCommandShortAlias.split(/=(.*)/);
115
+ potentialGlobalCommandShortAlias.split(/=(.*)/) as [
116
+ string,
117
+ string | undefined,
118
+ ];
116
119
  }
117
120
 
118
121
  if (!globalCommandsByShortAlias) {
@@ -169,8 +172,8 @@ function scanForNextCommandClause(
169
172
 
170
173
  // looking for:
171
174
  // <global_[modifier_]command_name>
172
- if (currentArg.startsWith("--")) {
173
- const potentialCommandName = currentArg.slice(2);
175
+ if (currentArg!.startsWith("--")) {
176
+ const potentialCommandName = currentArg!.slice(2);
174
177
 
175
178
  // check for Global[Modifier]Command name
176
179
  const command = commandsByName.get(potentialCommandName);
@@ -188,8 +191,8 @@ function scanForNextCommandClause(
188
191
  };
189
192
  }
190
193
  }
191
- if (!includeSubCommands || (currentArg[0] === "-")) {
192
- unusedArgs.push(currentArg);
194
+ if (!includeSubCommands || (currentArg![0] === "-")) {
195
+ unusedArgs.push(currentArg!);
193
196
  continue;
194
197
  }
195
198
 
@@ -202,11 +205,11 @@ function scanForNextCommandClause(
202
205
  let potentialMemberSubCommandName: string | undefined;
203
206
  let potentialMemberCommandNameArgumentUsed = false;
204
207
 
205
- if (potentialGroupCommandName.includes(":")) {
208
+ if (potentialGroupCommandName!.includes(":")) {
206
209
  // looking for:
207
210
  // <group_command_name>:<member_sub_command_name>
208
211
  [potentialGroupCommandName, potentialMemberSubCommandName] =
209
- potentialGroupCommandName.split(":");
212
+ potentialGroupCommandName!.split(":") as [string, string | undefined];
210
213
  } else if (pendingArgs.length > 0) {
211
214
  // looking for:
212
215
  // <group_command_name> <member_sub_command_name>
@@ -250,9 +253,9 @@ function scanForNextCommandClause(
250
253
  //
251
254
  // if we find it is a group command we skip it as we didn't find a member command
252
255
  // following it the logic above
253
- const command = commandsByName.get(currentArg);
256
+ const command = commandsByName.get(currentArg!);
254
257
  if (command && !isGroupCommand(command)) {
255
- logger.debug("Found sub-command name: %s", currentArg);
258
+ logger.debug("Found sub-command name: %s", currentArg!);
256
259
  return {
257
260
  commandClause: {
258
261
  command,
@@ -263,7 +266,7 @@ function scanForNextCommandClause(
263
266
  }
264
267
 
265
268
  // if we got here we haven't found a command so add the current arg to unused args
266
- unusedArgs.push(currentArg);
269
+ unusedArgs.push(currentArg!);
267
270
  }
268
271
  // if we got here we haven't found a command and we have processed all provided args
269
272
  return {
@@ -120,7 +120,7 @@ function validatePrimitiveValue(
120
120
  (argument.type === ArgumentValueTypeName.STRING) &&
121
121
  argument.isCaseInsensitive
122
122
  ) {
123
- searchValue = convertedValue.toLowerCase();
123
+ searchValue = (convertedValue as string).toLowerCase();
124
124
  allowableValues = argument.allowableValues.map((v) =>
125
125
  (v as string).toLowerCase()
126
126
  );
@@ -136,7 +136,7 @@ function validatePrimitiveValue(
136
136
  }
137
137
  }
138
138
  if (argument.minValueInclusive !== undefined) {
139
- if (convertedValue < argument.minValueInclusive) {
139
+ if ((convertedValue as number) < argument.minValueInclusive) {
140
140
  return {
141
141
  invalidArgument: {
142
142
  argument,
@@ -148,7 +148,7 @@ function validatePrimitiveValue(
148
148
  }
149
149
 
150
150
  if (argument.maxValueInclusive !== undefined) {
151
- if (convertedValue > argument.maxValueInclusive) {
151
+ if ((convertedValue as number) > argument.maxValueInclusive) {
152
152
  return {
153
153
  invalidArgument: {
154
154
  argument,
@@ -275,7 +275,7 @@ function validateObjectValue(
275
275
  const convertedObjectValue: ArgumentValues = {};
276
276
 
277
277
  for (let i = 0; i < argument.properties.length; i++) {
278
- const propertyArg = argument.properties[i];
278
+ const propertyArg = argument.properties[i]!;
279
279
  const propertyValue = objectValue[propertyArg.name];
280
280
 
281
281
  if (propertyValue === undefined) {
@@ -306,7 +306,10 @@ function validateObjectValue(
306
306
  },
307
307
  };
308
308
  }
309
- validationResult = validateArrayValue(propertyArg, propertyValue);
309
+ validationResult = validateArrayValue(
310
+ propertyArg as ComplexOption | SubCommandArgument,
311
+ propertyValue,
312
+ );
310
313
  } else if (typeof propertyValue === "object") {
311
314
  if (propertyArg.type !== ComplexValueTypeName.COMPLEX) {
312
315
  return {
@@ -468,6 +471,24 @@ function doSubCommandArgumentValidation(
468
471
  invalidArguments.push(validationResult.invalidArgument);
469
472
  return undefined;
470
473
  }
474
+ if (argument.validate && validationResult.validValue !== undefined) {
475
+ const customError = argument.validate(
476
+ validationResult.validValue as
477
+ | ArgumentValueType
478
+ | ArgumentValues
479
+ | Array<ArgumentValues>,
480
+ );
481
+ if (customError !== undefined) {
482
+ invalidArguments.push({
483
+ argument,
484
+ name: argument.name,
485
+ value: validationResult.validValue as PopulatedArgumentValueType,
486
+ reason: InvalidArgumentReason.CUSTOM_VALIDATION,
487
+ message: customError,
488
+ });
489
+ return undefined;
490
+ }
491
+ }
471
492
  return validationResult.validValue as
472
493
  | PopulatedArgumentValueType
473
494
  | PopulatedArgumentValues
@@ -574,6 +595,24 @@ export function validateGlobalCommandArgumentValue(
574
595
  return undefined;
575
596
  }
576
597
 
598
+ if (
599
+ globalCommandArgument.validate &&
600
+ validationResult.validValue !== undefined
601
+ ) {
602
+ const customError = globalCommandArgument.validate(
603
+ validationResult.validValue as ArgumentValueType,
604
+ );
605
+ if (customError !== undefined) {
606
+ invalidArguments.push({
607
+ argument: globalCommandArgument,
608
+ name: globalCommand.name,
609
+ value: validationResult.validValue,
610
+ reason: InvalidArgumentReason.CUSTOM_VALIDATION,
611
+ message: customError,
612
+ });
613
+ return undefined;
614
+ }
615
+ }
577
616
  return validationResult.validValue as PopulatedArgumentSingleValueType;
578
617
  }
579
618
 
@@ -641,6 +680,11 @@ export function getInvalidArgumentString(
641
680
  case InvalidArgumentReason.OPTION_IS_COMPLEX:
642
681
  invalidString = "(specified option is complex)";
643
682
  break;
683
+ case InvalidArgumentReason.CUSTOM_VALIDATION:
684
+ invalidString = invalidArgument.message
685
+ ? `(custom validation: ${invalidArgument.message})`
686
+ : "(custom validation failed)";
687
+ break;
644
688
  default:
645
689
  invalidString = "";
646
690
  break;
@@ -49,7 +49,7 @@ export default function populateGlobalCommandValue(
49
49
 
50
50
  // don't use potentialArg if it cannot be a boolean value...
51
51
  if (argument.type === ArgumentValueTypeName.BOOLEAN) {
52
- const firstPotentialArgLower = firstPotentialArg.toLowerCase();
52
+ const firstPotentialArgLower = firstPotentialArg!.toLowerCase();
53
53
 
54
54
  if (
55
55
  (firstPotentialArgLower !== "true") &&
@@ -175,14 +175,14 @@ class ParseContext {
175
175
 
176
176
  for (let i = 0; i < complexPathElements.length; i++) {
177
177
  let arrayIndexString: string | undefined;
178
- let complexPathElement = complexPathElements[i];
178
+ let complexPathElement = complexPathElements[i]!;
179
179
 
180
180
  // look for array index
181
181
  if (complexPathElement.endsWith("]")) {
182
182
  [complexPathElement, arrayIndexString] = complexPathElement.slice(
183
183
  0,
184
184
  complexPathElement.length - 1,
185
- ).split("[");
185
+ ).split("[") as [string, string | undefined];
186
186
  }
187
187
 
188
188
  let lookupPath = this.subCommand.name;
@@ -237,7 +237,7 @@ class ParseContext {
237
237
  }
238
238
  option = this.optionLookupMapsByPath.get(lookupPath)!.get(
239
239
  complexPathElement,
240
- );
240
+ ) as Option | ComplexOption | undefined;
241
241
 
242
242
  // now we are parsing properties of complex options it is an error if we cannot match an option name or alias
243
243
  if (option === undefined) {
@@ -423,8 +423,8 @@ class ParseContext {
423
423
  if (typeof pathElement === "string") {
424
424
  if (Array.isArray(optionValue)) {
425
425
  optionValue = (optionValue as Array<PopulatedArgumentValues>)[
426
- optionValue.length - 1
427
- ][pathElement as string] as
426
+ (optionValue as Array<PopulatedArgumentValues>).length - 1
427
+ ]![pathElement as string] as
428
428
  | Array<PopulatedArgumentValues>
429
429
  | PopulatedArgumentValues
430
430
  | PopulatedArgumentValueType;
@@ -459,7 +459,8 @@ class ParseContext {
459
459
  * @param value the value to attempt to set.
460
460
  */
461
461
  setPositionalValue(value: ArgumentSingleValueType): boolean {
462
- const positional = this.subCommand.positionals[this.currentPositionalIndex];
462
+ const positional = this.subCommand
463
+ .positionals[this.currentPositionalIndex]!;
463
464
  const currentValue = this.populatedArgumentValues[positional.name];
464
465
 
465
466
  // if we haven't populated the argument value before...
@@ -536,7 +537,7 @@ class ParseContext {
536
537
  // check for option AND value
537
538
  if (potentialOptionPath.includes("=")) {
538
539
  [potentialOptionPath, potentialOptionValue] = potentialOptionPath
539
- .split(/=(.*)/);
540
+ .split(/=(.*)/) as [string, string | undefined, ...string[]];
540
541
 
541
542
  // check for illegal syntax e.g. --optionName=
542
543
  if (potentialOptionValue === "") {
@@ -623,7 +624,7 @@ export default function populateSubCommandValues(
623
624
  const unusedArgs: Array<string> = [];
624
625
 
625
626
  for (let i = 0; i < potentialArgs.length; i++) {
626
- const potentialArg = potentialArgs[i];
627
+ const potentialArg = potentialArgs[i]!;
627
628
 
628
629
  // parse the next arg
629
630
  parseContext.parse(potentialArg);