@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
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
 
@@ -36,6 +36,7 @@ function validateValueType(
36
36
  value: ArgumentSingleValueType,
37
37
  argumentName: string,
38
38
  isDefaultValue: boolean,
39
+ isRangeValue: boolean,
39
40
  ): void {
40
41
  switch (type) {
41
42
  case ArgumentValueTypeName.BOOLEAN:
@@ -51,7 +52,7 @@ function validateValueType(
51
52
  if ((typeof value !== "number") || !Number.isFinite(value)) {
52
53
  throw new Error(
53
54
  `Specified ${
54
- isDefaultValue ? "default" : "allowable"
55
+ isDefaultValue ? "default" : isRangeValue ? "range" : "allowable"
55
56
  } value: '${value}' for argument: '${argumentName}' should be a finite number`,
56
57
  );
57
58
  }
@@ -60,7 +61,7 @@ function validateValueType(
60
61
  if ((typeof value !== "number") || !Number.isInteger(value)) {
61
62
  throw new Error(
62
63
  `Specified ${
63
- isDefaultValue ? "default" : "allowable"
64
+ isDefaultValue ? "default" : isRangeValue ? "range" : "allowable"
64
65
  } value: '${value}' for argument: '${argumentName}' should be an integer`,
65
66
  );
66
67
  }
@@ -97,7 +98,7 @@ function isNameLegal(name: string): boolean {
97
98
  function isConfigurationKeyLegal(configurationKey: string): boolean {
98
99
  return (configurationKey.length > 0) &&
99
100
  isUppercaseAlphaNumericOrUnderscore(configurationKey) &&
100
- !configurationKey[0].match(/[0-9]/);
101
+ !configurationKey[0]!.match(/[0-9]/);
101
102
  }
102
103
 
103
104
  function validateArgument(argument: Argument, name: string): void {
@@ -112,10 +113,73 @@ function validateArgument(argument: Argument, name: string): void {
112
113
  );
113
114
  }
114
115
  if (argument.allowableValues) {
116
+ if (argument.type === ArgumentValueTypeName.BOOLEAN) {
117
+ throw new Error(
118
+ `Illegal type: '${argument.type}' for argument: '${name}' with allowable values specified`,
119
+ );
120
+ }
115
121
  argument.allowableValues.forEach((value) => {
116
- validateValueType(argument.type, value, name, false);
122
+ validateValueType(argument.type, value, name, false, false);
117
123
  });
118
124
  }
125
+ if (argument.isCaseInsensitive !== undefined) {
126
+ if (
127
+ (argument.type === ArgumentValueTypeName.BOOLEAN) &&
128
+ (argument.isCaseInsensitive === false)
129
+ ) {
130
+ throw new Error(
131
+ `Illegal type: '${argument.type}' for argument: '${name}' must always be case insensitive`,
132
+ );
133
+ } else if (argument.type !== ArgumentValueTypeName.STRING) {
134
+ throw new Error(
135
+ `Illegal type: '${argument.type}' for argument: '${name}' with case insensitive specified`,
136
+ );
137
+ }
138
+ }
139
+ if (argument.minValueInclusive !== undefined) {
140
+ if (
141
+ (argument.type !== ArgumentValueTypeName.NUMBER) &&
142
+ (argument.type !== ArgumentValueTypeName.INTEGER)
143
+ ) {
144
+ throw new Error(
145
+ `Illegal type: '${argument.type}' for argument: '${name}' with min value specified`,
146
+ );
147
+ }
148
+ if (argument.allowableValues) {
149
+ throw new Error(
150
+ `Illegal combination of allowable values and min value for argument: '${name}'`,
151
+ );
152
+ }
153
+ validateValueType(
154
+ argument.type,
155
+ argument.minValueInclusive,
156
+ name,
157
+ false,
158
+ true,
159
+ );
160
+ }
161
+ if (argument.maxValueInclusive !== undefined) {
162
+ if (
163
+ (argument.type !== ArgumentValueTypeName.NUMBER) &&
164
+ (argument.type !== ArgumentValueTypeName.INTEGER)
165
+ ) {
166
+ throw new Error(
167
+ `Illegal type: '${argument.type}' for argument: '${name}' with max value specified`,
168
+ );
169
+ }
170
+ if (argument.allowableValues) {
171
+ throw new Error(
172
+ `Illegal combination of allowable values and max value for argument: '${name}'`,
173
+ );
174
+ }
175
+ validateValueType(
176
+ argument.type,
177
+ argument.maxValueInclusive,
178
+ name,
179
+ false,
180
+ true,
181
+ );
182
+ }
119
183
  }
120
184
 
121
185
  function validateSubCommandArgument(
@@ -304,7 +368,7 @@ export default class CommandValidator {
304
368
  }
305
369
  if (subCommand.positionals) {
306
370
  for (let i = 0; i < subCommand.positionals.length; i += 1) {
307
- const positional = subCommand.positionals[i];
371
+ const positional = subCommand.positionals[i]!;
308
372
  validateSubCommandArgument(positional);
309
373
  if (argumentNames.includes(positional.name)) {
310
374
  throw new Error(
@@ -332,7 +396,7 @@ export default class CommandValidator {
332
396
  command: Command,
333
397
  argumentAncestry: Array<SubCommandArgument>,
334
398
  ) {
335
- const argument = argumentAncestry[argumentAncestry.length - 1];
399
+ const argument = argumentAncestry[argumentAncestry.length - 1]!;
336
400
  if (command.enableConfiguration === true) {
337
401
  if (
338
402
  (argument.configurationKey !== undefined) &&
@@ -12,6 +12,7 @@ import type {
12
12
  import getLogger from "../util/logger.ts";
13
13
  import {
14
14
  printCommandExecutionError,
15
+ printNoCommandRecognisedError,
15
16
  printNoCommandSpecifiedError,
16
17
  printParseResultError,
17
18
  printUnusedArgsWarning,
@@ -32,9 +33,35 @@ import type GroupCommand from "../api/command/GroupCommand.ts";
32
33
  import type SubCommand from "../api/command/SubCommand.ts";
33
34
  import type GlobalCommand from "../api/command/GlobalCommand.ts";
34
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";
35
38
 
36
39
  const logger = getLogger("runner");
37
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
+
38
65
  /**
39
66
  * Execute the {@link Command} with arguments contained in the provided {@link ParseResult}.
40
67
  *
@@ -55,13 +82,19 @@ async function executeParsedCommand(
55
82
  "Executing group command with name: %s",
56
83
  parseResult!.groupCommand!.name,
57
84
  );
58
- if (configurationServiceProvider?.keyValueServiceEnabled) {
85
+ if (
86
+ configurationServiceProvider?.keyValueServiceEnabled ||
87
+ configurationServiceProvider?.secretServiceEnabled
88
+ ) {
59
89
  configurationServiceProvider.setCommandKeyValueScope(
60
90
  parseResult!.groupCommand!.name,
61
91
  );
62
92
  }
63
93
  await parseResult.groupCommand.execute(context);
64
- if (configurationServiceProvider?.keyValueServiceEnabled) {
94
+ if (
95
+ configurationServiceProvider?.keyValueServiceEnabled ||
96
+ configurationServiceProvider?.secretServiceEnabled
97
+ ) {
65
98
  await configurationServiceProvider.clearKeyValueScope();
66
99
  }
67
100
  }
@@ -72,7 +105,10 @@ async function executeParsedCommand(
72
105
  parseResult!.populatedArgumentValues,
73
106
  );
74
107
 
75
- if (configurationServiceProvider?.keyValueServiceEnabled) {
108
+ if (
109
+ configurationServiceProvider?.keyValueServiceEnabled ||
110
+ configurationServiceProvider?.secretServiceEnabled
111
+ ) {
76
112
  configurationServiceProvider.setCommandKeyValueScope(
77
113
  parseResult.command.name,
78
114
  );
@@ -88,10 +124,16 @@ async function executeParsedCommand(
88
124
  parseResult.populatedArgumentValues as ArgumentSingleValueType,
89
125
  );
90
126
  }
91
- if (configurationServiceProvider?.keyValueServiceEnabled) {
127
+ if (
128
+ configurationServiceProvider?.keyValueServiceEnabled ||
129
+ configurationServiceProvider?.secretServiceEnabled
130
+ ) {
92
131
  await configurationServiceProvider.clearKeyValueScope();
93
132
  }
94
133
  } catch (err) {
134
+ if ((err as Error).message === "Interrupted") {
135
+ throw err;
136
+ }
95
137
  await printCommandExecutionError(
96
138
  context,
97
139
  parseResult,
@@ -154,7 +196,7 @@ async function findAndExecuteGlobalModifierCommands(
154
196
  ) {
155
197
  // parse and fast fail on error
156
198
  const defaultArgumentValues = configurationServiceProvider
157
- ? configurationServiceProvider
199
+ ? await configurationServiceProvider
158
200
  .getDefaultArgumentValues(
159
201
  context.cliConfig,
160
202
  globalModifierCommandClause.command,
@@ -194,7 +236,7 @@ async function findAndExecuteGlobalModifierCommands(
194
236
  for (const globalModifierCommand of remainingGlobalModifierCommands) {
195
237
  // if there is a config entry run this by default even though no arguments were provided on the command line
196
238
  const defaultArgumentValues = configurationServiceProvider
197
- ? configurationServiceProvider
239
+ ? await configurationServiceProvider
198
240
  .getDefaultArgumentValues(context.cliConfig, globalModifierCommand)
199
241
  : undefined;
200
242
 
@@ -273,7 +315,7 @@ async function findAndExecuteNonModifierCommand(
273
315
 
274
316
  if (scanResult.nonModifierCommandClause) {
275
317
  const defaultArgumentValues = configurationServiceProvider
276
- ? configurationServiceProvider
318
+ ? await configurationServiceProvider
277
319
  .getDefaultArgumentValues(
278
320
  context.cliConfig,
279
321
  scanResult.nonModifierCommandClause.command,
@@ -293,12 +335,20 @@ async function findAndExecuteNonModifierCommand(
293
335
  }
294
336
 
295
337
  if (parseResult.invalidArguments.length > 0) {
296
- await printParseResultError(context, parseResult);
297
- return {
298
- runState: RunState.PARSE_ERROR,
299
- command: parseResult.command,
300
- invalidArguments: parseResult.invalidArguments,
301
- };
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
+ }
302
352
  }
303
353
 
304
354
  // add any unused args back to the list of still unused args
@@ -310,7 +360,7 @@ async function findAndExecuteNonModifierCommand(
310
360
  for (const nonModifierCommand of nonModifierCommandsByName.values()) {
311
361
  // if there is a config entry run this by default even though no arguments were provided on the command line
312
362
  const defaultArgumentValues = configurationServiceProvider
313
- ? configurationServiceProvider
363
+ ? await configurationServiceProvider
314
364
  .getDefaultArgumentValues(context.cliConfig, nonModifierCommand)
315
365
  : undefined;
316
366
 
@@ -328,12 +378,20 @@ async function findAndExecuteNonModifierCommand(
328
378
  }
329
379
 
330
380
  if (parseResult.invalidArguments.length > 0) {
331
- await printParseResultError(context, parseResult);
332
- return {
333
- runState: RunState.PARSE_ERROR,
334
- command: parseResult.command,
335
- invalidArguments: parseResult.invalidArguments,
336
- };
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
+ }
337
395
  }
338
396
 
339
397
  break;
@@ -375,7 +433,7 @@ async function findAndExecuteDefaultNonModifierCommand(
375
433
  context: Context,
376
434
  ): Promise<RunResult | undefined> {
377
435
  const defaultArgumentValues = configurationServiceProvider
378
- ? configurationServiceProvider
436
+ ? await configurationServiceProvider
379
437
  .getDefaultArgumentValues(context.cliConfig, defaultNonModifierCommand)
380
438
  : undefined;
381
439
  const isGlobal = isGlobalCommand(defaultNonModifierCommand);
@@ -447,14 +505,23 @@ async function findAndExecuteDefaultNonModifierCommand(
447
505
  }
448
506
  }
449
507
 
450
- // if there were invalid arguments, treat this as the error
508
+ // if there were invalid arguments, attempt prompting before treating as error
451
509
  if (lastErroneousParseResult) {
452
- await printParseResultError(context, lastErroneousParseResult, true);
453
- return {
454
- runState: RunState.PARSE_ERROR,
455
- command: defaultNonModifierCommand,
456
- invalidArguments: lastErroneousParseResult.invalidArguments,
457
- };
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
+ }
458
525
  }
459
526
 
460
527
  // give up if still nothing found
@@ -603,14 +670,20 @@ export async function run(
603
670
 
604
671
  logger.debug("Initialising service with ID: %s", serviceProvider.serviceId);
605
672
 
606
- if (configurationServiceProvider?.keyValueServiceEnabled) {
673
+ if (
674
+ configurationServiceProvider?.keyValueServiceEnabled ||
675
+ configurationServiceProvider?.secretServiceEnabled
676
+ ) {
607
677
  configurationServiceProvider.setServiceKeyValueScope(
608
678
  serviceProvider.serviceId,
609
679
  );
610
680
  }
611
681
 
612
682
  await serviceProvider.initService(context);
613
- if (configurationServiceProvider?.keyValueServiceEnabled) {
683
+ if (
684
+ configurationServiceProvider?.keyValueServiceEnabled ||
685
+ configurationServiceProvider?.secretServiceEnabled
686
+ ) {
614
687
  await configurationServiceProvider.clearKeyValueScope();
615
688
  }
616
689
  }
@@ -690,7 +763,15 @@ export async function run(
690
763
  }
691
764
 
692
765
  // give up if we haven't successfully parsed a non-modifier Command by now
693
- await printNoCommandSpecifiedError(context);
766
+ if (availableArgs.length > 0) {
767
+ await printNoCommandRecognisedError(
768
+ context,
769
+ commandRegistry,
770
+ availableArgs.flat(),
771
+ );
772
+ } else {
773
+ await printNoCommandSpecifiedError(context);
774
+ }
694
775
  return {
695
776
  runState: RunState.NO_COMMAND,
696
777
  };
@@ -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 {