@flowscripter/dynamic-cli-framework 1.0.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 (133) hide show
  1. package/.github/workflows/check-bun-dependencies.yml +9 -0
  2. package/.github/workflows/lint-pr-message.yml +11 -0
  3. package/.github/workflows/release-bun-library.yml +8 -0
  4. package/.github/workflows/validate-bun-library-pr.yml +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +1475 -0
  7. package/bun.lock +63 -0
  8. package/index.ts +89 -0
  9. package/package.json +23 -0
  10. package/src/api/CLI.ts +17 -0
  11. package/src/api/CLIConfig.ts +19 -0
  12. package/src/api/Context.ts +25 -0
  13. package/src/api/RunResult.ts +142 -0
  14. package/src/api/argument/Argument.ts +36 -0
  15. package/src/api/argument/ArgumentValueTypes.ts +87 -0
  16. package/src/api/argument/ComplexOption.ts +31 -0
  17. package/src/api/argument/GlobalCommandArgument.ts +17 -0
  18. package/src/api/argument/Option.ts +32 -0
  19. package/src/api/argument/Positional.ts +27 -0
  20. package/src/api/argument/SubCommandArgument.ts +20 -0
  21. package/src/api/command/Command.ts +22 -0
  22. package/src/api/command/GlobalCommand.ts +33 -0
  23. package/src/api/command/GlobalModifierCommand.ts +12 -0
  24. package/src/api/command/GroupCommand.ts +20 -0
  25. package/src/api/command/SubCommand.ts +41 -0
  26. package/src/api/command/UsageExample.ts +21 -0
  27. package/src/api/service/ServiceProvider.ts +52 -0
  28. package/src/api/service/core/AsciiBannerGeneratorService.ts +35 -0
  29. package/src/api/service/core/KeyValueService.ts +28 -0
  30. package/src/api/service/core/PrinterService.ts +266 -0
  31. package/src/api/service/core/ShutdownService.ts +12 -0
  32. package/src/api/service/core/SyntaxHighlighterService.ts +42 -0
  33. package/src/cli/BaseCLI.ts +312 -0
  34. package/src/cli/DefaultRuntimeCLI.ts +54 -0
  35. package/src/command/MultiCommandCliHelpCommand.ts +394 -0
  36. package/src/command/SingleCommandCliHelpCommand.ts +183 -0
  37. package/src/command/UsageCommand.ts +35 -0
  38. package/src/command/VersionCommand.ts +22 -0
  39. package/src/launcher.ts +127 -0
  40. package/src/runtime/DefaultContext.ts +32 -0
  41. package/src/runtime/argument/ArgumentTypeGuards.ts +8 -0
  42. package/src/runtime/command/CommandTypeGuards.ts +30 -0
  43. package/src/runtime/command/CommandValidator.ts +549 -0
  44. package/src/runtime/parser.ts +219 -0
  45. package/src/runtime/registry/CommandRegistry.ts +121 -0
  46. package/src/runtime/registry/DefaultCommandRegistry.ts +318 -0
  47. package/src/runtime/registry/DefaultServiceProviderRegistry.ts +26 -0
  48. package/src/runtime/registry/ServiceProviderRegistry.ts +11 -0
  49. package/src/runtime/runner.ts +697 -0
  50. package/src/runtime/scanner.ts +417 -0
  51. package/src/runtime/values/ValuePopulationResult.ts +41 -0
  52. package/src/runtime/values/argumentValueMerge.ts +201 -0
  53. package/src/runtime/values/argumentValueValidation.ts +610 -0
  54. package/src/runtime/values/globalCommandValuePopulation.ts +96 -0
  55. package/src/runtime/values/subCommandValuePopulation.ts +680 -0
  56. package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +45 -0
  57. package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +38 -0
  58. package/src/service/asciiBannerGenerator/standard.flf.json +3 -0
  59. package/src/service/banner/BannerServiceProvider.ts +99 -0
  60. package/src/service/banner/command/NoBannerCommand.ts +42 -0
  61. package/src/service/configuration/ConfigurationServiceProvider.ts +571 -0
  62. package/src/service/configuration/DefaultKeyValueService.ts +60 -0
  63. package/src/service/configuration/command/ConfigCommand.ts +43 -0
  64. package/src/service/configuration/command/DumpConfigCommand.ts +47 -0
  65. package/src/service/printer/DefaultPrinterService.ts +256 -0
  66. package/src/service/printer/PrinterServiceProvider.ts +61 -0
  67. package/src/service/printer/command/DarkModeCommand.ts +43 -0
  68. package/src/service/printer/command/LogLevelCommand.ts +68 -0
  69. package/src/service/printer/command/NoColorCommand.ts +43 -0
  70. package/src/service/printer/terminal/Ansi.ts +10 -0
  71. package/src/service/printer/terminal/Color.ts +14 -0
  72. package/src/service/printer/terminal/Progress.ts +373 -0
  73. package/src/service/printer/terminal/Spinner.ts +100 -0
  74. package/src/service/printer/terminal/Styler.ts +5 -0
  75. package/src/service/printer/terminal/Terminal.ts +8 -0
  76. package/src/service/printer/terminal/Theme.ts +34 -0
  77. package/src/service/printer/terminal/TtyStyler.ts +39 -0
  78. package/src/service/printer/terminal/TtyTerminal.ts +65 -0
  79. package/src/service/shutdown/DefaultShutdownService.ts +9 -0
  80. package/src/service/shutdown/ShutdownServiceProvider.ts +55 -0
  81. package/src/service/syntaxHighlighter/DefaultSyntaxHighlighterService.ts +52 -0
  82. package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +51 -0
  83. package/src/util/configHelper.ts +95 -0
  84. package/src/util/envVarHelper.ts +338 -0
  85. package/src/util/helpHelper.ts +751 -0
  86. package/src/util/logger.ts +84 -0
  87. package/src/util/runnerHelper.ts +112 -0
  88. package/tests/cli/BaseCLI.test.ts +344 -0
  89. package/tests/command/MultiCommandCliHelpCommand.test.ts +650 -0
  90. package/tests/command/SingleCommandCliHelpCommand.test.ts +384 -0
  91. package/tests/command/UsageCommand.test.ts +53 -0
  92. package/tests/command/VersionCommand.test.ts +36 -0
  93. package/tests/fixtures/CLIConfig.ts +9 -0
  94. package/tests/fixtures/Command.ts +229 -0
  95. package/tests/fixtures/CommandRegistry.ts +8 -0
  96. package/tests/fixtures/ConfigurationServiceProvider.ts +28 -0
  97. package/tests/fixtures/Context.ts +33 -0
  98. package/tests/fixtures/ServiceProviderRegistry.ts +16 -0
  99. package/tests/fixtures/StreamString.ts +37 -0
  100. package/tests/fixtures/util.ts +41 -0
  101. package/tests/runtime/DefaultContext.test.ts +20 -0
  102. package/tests/runtime/argument/ArgumentTypeGuards.test.ts +49 -0
  103. package/tests/runtime/command/CommandTypeGuards.test.ts +43 -0
  104. package/tests/runtime/command/CommandValidator.test.ts +816 -0
  105. package/tests/runtime/parser.test.ts +766 -0
  106. package/tests/runtime/registry/DefaultCommandRegistry.test.ts +166 -0
  107. package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +42 -0
  108. package/tests/runtime/runner.test.ts +1298 -0
  109. package/tests/runtime/scanner.test.ts +1113 -0
  110. package/tests/runtime/values/argumentValueMerge.test.ts +314 -0
  111. package/tests/runtime/values/argumentValueValidation.test.ts +1020 -0
  112. package/tests/runtime/values/globalCommandValuePopulation.test.ts +662 -0
  113. package/tests/runtime/values/subCommandValuePopulation.test.ts +2658 -0
  114. package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +22 -0
  115. package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +70 -0
  116. package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +22 -0
  117. package/tests/service/asciiBannerGenerator/small.flf.json +3 -0
  118. package/tests/service/configuration/ConfigurationServiceProvider.test.ts +249 -0
  119. package/tests/service/configuration/DefaultKeyValueService.test.ts +65 -0
  120. package/tests/service/launchBanner/BannerServiceProvider.test.ts +49 -0
  121. package/tests/service/launchBanner/__snapshots__/BannerServiceProvider.test.ts.snap +3 -0
  122. package/tests/service/launchBanner/command/NoBannerCommand.test.ts +21 -0
  123. package/tests/service/printer/DefaultPrinterService.test.ts +409 -0
  124. package/tests/service/printer/PrinterServiceProvider.test.ts +29 -0
  125. package/tests/service/printer/command/LogLevelCommand.test.ts +58 -0
  126. package/tests/service/printer/terminal/Progress.test.ts +40 -0
  127. package/tests/service/printer/terminal/Spinner.test.ts +57 -0
  128. package/tests/service/shutdown/ShutdownServiceProvider.test.ts +13 -0
  129. package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +123 -0
  130. package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +41 -0
  131. package/tests/util/configHelper.test.ts +275 -0
  132. package/tests/util/envVarHelper.test.ts +318 -0
  133. package/tests/util/logger.test.ts +10 -0
@@ -0,0 +1,571 @@
1
+ import process from "node:process";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { Stats } from "node:fs";
5
+ import type {
6
+ ServiceInfo,
7
+ ServiceProvider,
8
+ } from "../../api/service/ServiceProvider.ts";
9
+ import ConfigCommand from "./command/ConfigCommand.ts";
10
+ import type {
11
+ ArgumentSingleValueType,
12
+ ArgumentValues,
13
+ PopulatedArgumentValues,
14
+ PopulatedArgumentValueType,
15
+ } from "../../api/argument/ArgumentValueTypes.ts";
16
+ import getLogger from "../../util/logger.ts";
17
+ import type Context from "../../api/Context.ts";
18
+ import DumpConfigCommand from "./command/DumpConfigCommand.ts";
19
+ import { KEY_VALUE_SERVICE_ID } from "../../api/service/core/KeyValueService.ts";
20
+ import DefaultKeyValueService from "./DefaultKeyValueService.ts";
21
+ import type Command from "../../api/command/Command.ts";
22
+ import argumentValueMerge from "../../runtime/values/argumentValueMerge.ts";
23
+ import {
24
+ isGlobalCommand,
25
+ isGlobalModifierCommand,
26
+ isSubCommand,
27
+ } from "../../runtime/command/CommandTypeGuards.ts";
28
+ import type CLIConfig from "../../api/CLIConfig.ts";
29
+ import {
30
+ getGlobalCommandValueFromEnvVars,
31
+ getSubCommandValuesFromEnvVars,
32
+ } from "../../util/envVarHelper.ts";
33
+
34
+ const logger = getLogger("ConfigurationServiceProvider");
35
+
36
+ /**
37
+ * Provides:
38
+ *
39
+ * * Configuration of default command arguments using a configuration file and/or environment variables.
40
+ * * A generic key-value store for commands and services ({@link KeyValueService}).
41
+ *
42
+ * **Default Command Arguments**
43
+ *
44
+ * Configuration of default {@link ArgumentValues} for {@link Command} instances is supported if they
45
+ * have defined {@link Command.enableConfiguration} as `true`.
46
+ *
47
+ * Two sources of configuration are supported and both are expected to be manually managed by the user of the CLI.
48
+ *
49
+ * *Configuration File*
50
+ *
51
+ * A JSON file where the structure matches {@link ArgumentValues} and the defaults are stored
52
+ * under a top level `defaults` property. The second level of properties is used to refer to {@link Command.name}
53
+ * and the contained values are treated as command {@link ArgumentValues}. As an example:
54
+ *
55
+ * ```
56
+ * {
57
+ * "defaults": {
58
+ * "subCommand1": {
59
+ * "arg1": [
60
+ * 1,
61
+ * 2
62
+ * ],
63
+ * "arg2": {
64
+ * "arg3": "foo"
65
+ * }
66
+ * },
67
+ * "command2": {
68
+ * "arg4": true
69
+ * },
70
+ * "globalCommand": "globalArgumentValue"
71
+ * },
72
+ * "key-values": {
73
+ * ...
74
+ * }
75
+ * }
76
+ * ```
77
+ *
78
+ * The default location of the configuration file is `$HOME/.<application_name>.json`. If `$HOME` is not defined no
79
+ * default configuration will be used. The location of the configuration file can be modified via the
80
+ * {@link ConfigCommand}.
81
+ *
82
+ * *Environment Variables*
83
+ *
84
+ * Values are parsed using a key path defined by custom {@link Argument.configurationKey}
85
+ * values or using the default naming scheme defined within {@link Argument.configurationKey}.
86
+ *
87
+ * NOTE: Any values set by environment variables will override those sourced from the configuration file.
88
+ *
89
+ * The argument key path is derived for an argument (or nested argument) as follows:
90
+ *
91
+ * Argument configuration keys are concatenated with a `_` separator.
92
+ * Any arguments which support array values must by suffixed with `_` and an explicit array index.
93
+ * If the root argument in the path does not use a custom {@link Argument.configurationKey} then the key path
94
+ * is additionally suffixed with the {@link CLIConfig.name} and the {@link Command.name} with `_` separators.
95
+ * This is best explained with examples...
96
+ *
97
+ * No custom configuration key examples:
98
+ *
99
+ * * executable: `MyCLI`, command: `globalCommand1`, global command argument => environment variable: `MYCLI_GLOBALCOMMAND1`
100
+ * * executable: `MyCLI`, command: `command1`, simple root argument: `arg1` => environment variable: `MYCLI_COMMAND1_ARG1`
101
+ * * executable: `MyCLI`, command: `command1`, array root argument, 1st element: `arg2[0]` => environment variable: `MYCLI_COMMAND1_ARG2_0`
102
+ * * executable: `MyCLI`, command: `command1`, argument is a digit so it is by default suffixed with `_`: `3` => environment variable: `MYCLI_COMMAND1__3`
103
+ * * executable: `MyCLI`, command: `command1`, nested sub-argument: `arg1.arg2` => environment variable: `MYCLI_COMMAND1_ARG1_ARG2`
104
+ * * executable: `MyCLI`, command: `command1`, nested sub-argument with both levels being arrays and referring to the 2nd element of each: `arg1[1].arg2[1]` => environment variable: `MYCLI_COMMAND1_ARG1_1_ARG2_2`
105
+ *
106
+ * Custom configuration key at the root level (and therefore not prefixed with CLI and command names) examples:
107
+ *
108
+ * * executable: `MyCLI`, command: `globalCommand1`, global command argument`, global command argument configuration key: `FOO` => environment variable: `FOO`
109
+ * * executable: `MyCLI`, command: `command1`, simple root argument: `arg1`, arg1 configuration key: `FOO` => environment variable: `FOO`
110
+ * * executable: `MyCLI`, command: `command1`, array root argument, 1st element: `arg2[0]`, arg1 configuration key: `BAR` => environment variable: `BAR_0`
111
+ *
112
+ * Custom configuration key not at the root level (and therefore prefixed with CLI and command names) examples:
113
+ *
114
+ * * executable: `MyCLI`, command: `command1`, nested sub-argument: `arg1.arg2`, arg2 configuration key: `FOO` => environment variable: `MYCLI_COMMAND1_ARG1_FOO`
115
+ * * executable: `MyCLI`, command: `command1`, nested sub-argument with both levels being arrays and referring to the 2nd element of each: `arg1[1].arg2[1]`, arg2 configuration key: `BAR` => environment variable: `MYCLI_COMMAND1_ARG1_1_BAR_2`
116
+ *
117
+ * NOTE: Any default values from the above configuration sources will be overridden by any arguments provided on the command line.
118
+ *
119
+ * **Generic Key-Value Service**
120
+ *
121
+ * The same configuration file above is used to provide storage for the provided {@link KeyValueService}. The values
122
+ * are stored under a top level `key-values` property. These are not expected to be modified by the user of the CLI.
123
+ *
124
+ * The second and third level of properties is used to scope the key-values to specific command names
125
+ * (via {@link Command.name} values) and specific service IDs (via {@link ServiceInfo.serviceId} values).
126
+ * Both keys and values are string based.
127
+ *
128
+ * As an example:
129
+ * ```
130
+ * {
131
+ * "defaults": {
132
+ * ...
133
+ * },
134
+ * "key-values": {
135
+ * "commands": {
136
+ * "command1": {
137
+ * "foo1": "bar1",
138
+ * "foo2": "bar2"
139
+ * },
140
+ * "command2": {
141
+ * "foo1": "bar3"
142
+ * }
143
+ * },
144
+ * "services": {
145
+ * "service-id-1": {
146
+ * "foo1": "bar"
147
+ * }
148
+ * }
149
+ * }
150
+ * }
151
+ * ```
152
+ */
153
+ export default class ConfigurationServiceProvider implements ServiceProvider {
154
+ readonly serviceId: string = KEY_VALUE_SERVICE_ID;
155
+ readonly servicePriority: number;
156
+
157
+ public readonly envVarsEnabled: boolean;
158
+ public readonly configEnabled: boolean;
159
+ public readonly keyValueServiceEnabled: boolean;
160
+
161
+ // the location of the currently managed configuration data
162
+ public configLocation: string | undefined;
163
+
164
+ // the configuration data to be used by the CLI runner implementation when setting command defaults.
165
+ // ArgumentSingleValueType is for GlobalCommandArgument values, ArgumentValues is for
166
+ // SubCommandArgument values.
167
+ public defaultsData: Map<
168
+ string,
169
+ ArgumentValues | ArgumentSingleValueType
170
+ > = new Map();
171
+
172
+ // the configuration data to be used by the CLI runner implementation
173
+ // when updating command scoped access to key-value data via the key-value service.
174
+ #commandKeyValueData = new Map<string, Map<string, string>>();
175
+
176
+ // the configuration data to be used by the CLI runner implementation
177
+ // when updating service scoped access to key-value data via the key-value service.
178
+ #serviceKeyValueData = new Map<string, Map<string, string>>();
179
+
180
+ // the optional service providing scope limited key-value data to commands and other services
181
+ readonly #defaultKeyValueService: DefaultKeyValueService | undefined;
182
+
183
+ #currentCommandNameKeyValueScope: string | undefined;
184
+ #currentServiceIdKeyValueScope: string | undefined;
185
+
186
+ /**
187
+ * Create an instance of the service provider with the specified details.
188
+ *
189
+ * @param servicePriority the priority of the service.
190
+ * @param envVarsEnabled optionally support checking env variables for default argument values.
191
+ * @param configEnabled optionally enable configuration file support for default argument values.
192
+ * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configEnabled` must be true in this case
193
+ */
194
+ public constructor(
195
+ servicePriority: number,
196
+ envVarsEnabled = false,
197
+ configEnabled = false,
198
+ keyValueServiceEnabled = false,
199
+ ) {
200
+ if (!configEnabled && keyValueServiceEnabled) {
201
+ throw new Error(
202
+ "configEnabled must be true if keyValueServiceEnabled is true",
203
+ );
204
+ }
205
+ this.servicePriority = servicePriority;
206
+ this.envVarsEnabled = envVarsEnabled;
207
+ this.configEnabled = configEnabled;
208
+ this.keyValueServiceEnabled = keyValueServiceEnabled;
209
+ if (keyValueServiceEnabled) {
210
+ this.#defaultKeyValueService = new DefaultKeyValueService();
211
+ }
212
+ }
213
+
214
+ public provide(_cliConfig: CLIConfig): Promise<ServiceInfo> {
215
+ const commands: Array<Command> = [];
216
+
217
+ if (this.configEnabled) {
218
+ commands.push(new ConfigCommand(this, this.servicePriority));
219
+ commands.push(new DumpConfigCommand(this));
220
+ }
221
+ return Promise.resolve({
222
+ // this may be undefined if keyValueServiceEnabled is false
223
+ service: this.#defaultKeyValueService,
224
+ // this may be empty if configEnabled is false
225
+ commands,
226
+ });
227
+ }
228
+
229
+ public setConfigLocation(configLocation: string) {
230
+ this.configLocation = configLocation;
231
+ }
232
+
233
+ /**
234
+ * Retrieve the defaults command argument values (if any) for the provided {@link Command}.
235
+ *
236
+ * This will retrieve values both from the configuration location and environment variables.
237
+ *
238
+ * @param cliConfig the {@link CLIConfig} to use for retrieving configuration keys.
239
+ * @param command the {@link Command} instance to retrieve default argument values for.
240
+ */
241
+ public getDefaultArgumentValues(
242
+ cliConfig: CLIConfig,
243
+ command: Command,
244
+ ): PopulatedArgumentValues | PopulatedArgumentValueType | undefined {
245
+ if (!this.configEnabled) {
246
+ logger.debug(
247
+ "configuration of default values is not enabled",
248
+ );
249
+ return undefined;
250
+ }
251
+
252
+ logger.debug(
253
+ "getting default argument values for command: %s",
254
+ command.name,
255
+ );
256
+
257
+ if (
258
+ (command.enableConfiguration === undefined) ||
259
+ (command.enableConfiguration !== true)
260
+ ) {
261
+ logger.debug(
262
+ "enableConfiguration for command: %s is not true",
263
+ command.name,
264
+ );
265
+ return undefined;
266
+ }
267
+
268
+ if (isGlobalModifierCommand(command) || isGlobalCommand(command)) {
269
+ const configuredValue = this.defaultsData.get(
270
+ command.name,
271
+ ) as ArgumentSingleValueType;
272
+ const envVarValue = this.envVarsEnabled
273
+ ? getGlobalCommandValueFromEnvVars(
274
+ cliConfig,
275
+ command,
276
+ )
277
+ : undefined;
278
+ // default to environment variable value
279
+ if (envVarValue !== undefined) {
280
+ return envVarValue;
281
+ }
282
+
283
+ return configuredValue;
284
+ } else if (isSubCommand(command)) {
285
+ const configuredValues = this.defaultsData.get(
286
+ command.name,
287
+ ) as ArgumentValues;
288
+ const envVarValues = this.envVarsEnabled
289
+ ? getSubCommandValuesFromEnvVars(
290
+ cliConfig,
291
+ command,
292
+ )
293
+ : undefined;
294
+ if (envVarValues === undefined) {
295
+ return configuredValues;
296
+ }
297
+ if (configuredValues === undefined) {
298
+ return envVarValues;
299
+ }
300
+ return argumentValueMerge(
301
+ envVarValues,
302
+ configuredValues,
303
+ ) as PopulatedArgumentValues;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Indicate that the contained {@link DefaultKeyValueService} should be set to have a scope of the provided
309
+ * command name.
310
+ *
311
+ * @param commandName the name of the command to scope the key-value data to.
312
+ *
313
+ * @throws {Error} if an existing scope is set.
314
+ */
315
+ public setCommandKeyValueScope(commandName: string): void {
316
+ if (!this.keyValueServiceEnabled) {
317
+ throw new Error(
318
+ `Attempt to use KeyValueService which is not enabled`,
319
+ );
320
+ }
321
+ if (this.#currentCommandNameKeyValueScope) {
322
+ throw new Error(
323
+ `Attempt to set CommandNameKeyValueScope to ${commandName} when it is currently set to ${this.#currentCommandNameKeyValueScope}`,
324
+ );
325
+ }
326
+ if (this.#currentServiceIdKeyValueScope) {
327
+ throw new Error(
328
+ `Attempt to set CommandNameKeyValueScope to ${commandName} when ServiceIdKeyValueScope is currently set to ${this.#currentCommandNameKeyValueScope}`,
329
+ );
330
+ }
331
+ this.#currentCommandNameKeyValueScope = commandName;
332
+
333
+ logger.debug("currentCommandNameKeyValueScope: %s", commandName);
334
+
335
+ if (!this.#commandKeyValueData.has(commandName)) {
336
+ this.#commandKeyValueData.set(commandName, new Map());
337
+ }
338
+ this.#defaultKeyValueService!.setKeyValueData(
339
+ this.#commandKeyValueData.get(commandName)!,
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Indicate that the contained {@link DefaultKeyValueService} should be set to have a scope of the provided
345
+ * service ID.
346
+ *
347
+ * @param serviceId the ID of the service to scope the key-value data to.
348
+ *
349
+ * @throws {Error} if an existing scope is set.
350
+ */
351
+ public setServiceKeyValueScope(serviceId: string): void {
352
+ if (!this.keyValueServiceEnabled) {
353
+ throw new Error(
354
+ `Attempt to use KeyValueService which is not enabled`,
355
+ );
356
+ }
357
+ if (this.#currentServiceIdKeyValueScope) {
358
+ throw new Error(
359
+ `Attempt to set ServiceIdKeyValueScope to ${serviceId} when it is currently set to ${this.#currentServiceIdKeyValueScope}`,
360
+ );
361
+ }
362
+ if (this.#currentCommandNameKeyValueScope) {
363
+ throw new Error(
364
+ `Attempt to set ServiceIdKeyValueScope to ${serviceId} when CommandNameKeyValueScope is currently set to ${this.#currentServiceIdKeyValueScope}`,
365
+ );
366
+ }
367
+ this.#currentServiceIdKeyValueScope = serviceId;
368
+
369
+ logger.debug("currentServiceIdKeyValueScope: %s", serviceId);
370
+
371
+ if (!this.#serviceKeyValueData.has(serviceId)) {
372
+ this.#serviceKeyValueData.set(serviceId, new Map());
373
+ }
374
+ this.#defaultKeyValueService!.setKeyValueData(
375
+ this.#serviceKeyValueData.get(serviceId)!,
376
+ );
377
+ }
378
+
379
+ /**
380
+ * Indicate that the contained {@link DefaultKeyValueService} should have any current scope cleared.
381
+ *
382
+ * If a scope is currently set and changes have been made to the key-value data, they will be written
383
+ * to the current configuration location.
384
+ */
385
+ public async clearKeyValueScope(): Promise<void> {
386
+ if (!this.keyValueServiceEnabled) {
387
+ throw new Error(
388
+ `Attempt to use KeyValueService which is not enabled`,
389
+ );
390
+ }
391
+ if (this.#defaultKeyValueService!.isDirty()) {
392
+ if (this.configLocation === undefined) {
393
+ throw new Error(
394
+ "Attempt to write updated config with no configLocation set",
395
+ );
396
+ }
397
+ await this.#writeConfig(this.configLocation);
398
+ }
399
+ this.#currentCommandNameKeyValueScope = undefined;
400
+ this.#currentServiceIdKeyValueScope = undefined;
401
+
402
+ this.#defaultKeyValueService!.clearKeyValueData();
403
+ }
404
+
405
+ public async initService(context: Context): Promise<void> {
406
+ if (!this.configEnabled) {
407
+ return;
408
+ }
409
+
410
+ let isDefault = false;
411
+ if (this.configLocation === undefined) {
412
+ isDefault = true;
413
+ // default to `$HOME/.<application_name>.json`
414
+ const home = process.env["HOME"];
415
+ if (home) {
416
+ this.configLocation = path.join(
417
+ home,
418
+ `.${context.cliConfig.name.replace(/\W/g, "")}.json`,
419
+ );
420
+ }
421
+ }
422
+ if (this.configLocation === undefined) {
423
+ return;
424
+ }
425
+ let fileInfo: Stats;
426
+ try {
427
+ fileInfo = await fs.lstat(this.configLocation);
428
+ } catch (err) {
429
+ const error = err as Error;
430
+ if (isDefault) {
431
+ logger.debug(
432
+ "Default config file location: %s does not exist or is not visible - ignoring: %s",
433
+ this.configLocation,
434
+ error.message,
435
+ );
436
+ return;
437
+ } else {
438
+ throw new Error(
439
+ `Config file location: '${this.configLocation}' doesn't exist or not visible: ${error.message}`,
440
+ );
441
+ }
442
+ }
443
+ if (fileInfo.isDirectory()) {
444
+ throw new Error(
445
+ `Config file location: '${this.configLocation}' is a directory and not a file!`,
446
+ );
447
+ }
448
+
449
+ await this.#readConfig(this.configLocation);
450
+ }
451
+
452
+ async #readConfig(location: string): Promise<void> {
453
+ try {
454
+ logger.debug("Reading config from: %s", location);
455
+
456
+ const data = await fs.readFile(location);
457
+ const config = JSON.parse(data.toString());
458
+
459
+ if (config === undefined) {
460
+ return;
461
+ }
462
+ this.defaultsData.clear();
463
+ this.#commandKeyValueData.clear();
464
+ this.#serviceKeyValueData.clear();
465
+ if (config.defaults !== undefined) {
466
+ const defaults = config.defaults;
467
+ Object.keys(defaults).forEach((commandName) => {
468
+ this.defaultsData.set(
469
+ commandName,
470
+ defaults[commandName] as ArgumentValues,
471
+ );
472
+ logger.debug(
473
+ "Set default argument values for command name: %s",
474
+ commandName,
475
+ );
476
+ });
477
+ }
478
+ if (config["key-values"] !== undefined) {
479
+ const keyValues = config["key-values"];
480
+ if (keyValues.commands !== undefined) {
481
+ const commandKeyValues = keyValues.commands;
482
+ Object.keys(commandKeyValues).forEach((commandName) => {
483
+ this.#commandKeyValueData.set(
484
+ commandName,
485
+ new Map(Object.entries(commandKeyValues[commandName])),
486
+ );
487
+ logger.debug(
488
+ "Set default argument values for command name: %s",
489
+ commandName,
490
+ );
491
+ });
492
+ }
493
+ if (keyValues.services !== undefined) {
494
+ const serviceKeyValues = keyValues.services;
495
+ Object.keys(serviceKeyValues).forEach((serviceId) => {
496
+ this.#serviceKeyValueData.set(
497
+ serviceId,
498
+ new Map(Object.entries(serviceKeyValues[serviceId])),
499
+ );
500
+ logger.debug(
501
+ "Set default argument values for service ID: %s",
502
+ serviceId,
503
+ );
504
+ });
505
+ }
506
+ }
507
+ } catch (err) {
508
+ throw new Error(
509
+ `Failed to read config at location: '${this.configLocation}': ${err}`,
510
+ );
511
+ }
512
+ }
513
+
514
+ async #writeConfig(location: string): Promise<void> {
515
+ logger.debug("Writing config to: %s", location);
516
+
517
+ try {
518
+ await fs.writeFile(location, this.getConfigString());
519
+ } catch (err) {
520
+ throw new Error(
521
+ `Failed to write config at location: '${this.configLocation}': ${err}`,
522
+ );
523
+ }
524
+ }
525
+
526
+ public getConfigString(): string {
527
+ const config: {
528
+ defaults?: Record<string, ArgumentValues | ArgumentSingleValueType>;
529
+ "key-values"?: {
530
+ commands?: Record<string, Record<string, string>>;
531
+ services?: Record<string, Record<string, string>>;
532
+ };
533
+ } = {};
534
+
535
+ if (this.defaultsData.size > 0) {
536
+ config.defaults = Object.fromEntries(this.defaultsData);
537
+ }
538
+ if (
539
+ (this.#commandKeyValueData.size > 0) ||
540
+ (this.#serviceKeyValueData.size > 0)
541
+ ) {
542
+ config["key-values"] = {};
543
+ if (this.#commandKeyValueData.size > 0) {
544
+ config["key-values"].commands = {};
545
+ for (
546
+ const [commandName, keyValueData] of this.#commandKeyValueData
547
+ .entries()
548
+ ) {
549
+ if (keyValueData.size > 0) {
550
+ config["key-values"].commands[commandName] = Object.fromEntries(
551
+ keyValueData,
552
+ );
553
+ }
554
+ }
555
+ }
556
+ if (this.#serviceKeyValueData.size > 0) {
557
+ config["key-values"].services = {};
558
+ for (
559
+ const [serviceId, keyValueData] of this.#serviceKeyValueData.entries()
560
+ ) {
561
+ if (keyValueData.size > 0) {
562
+ config["key-values"].services[serviceId] = Object.fromEntries(
563
+ keyValueData,
564
+ );
565
+ }
566
+ }
567
+ }
568
+ }
569
+ return JSON.stringify(config, null, 2);
570
+ }
571
+ }
@@ -0,0 +1,60 @@
1
+ import type KeyValueService from "../../api/service/core/KeyValueService.ts";
2
+
3
+ export default class DefaultKeyValueService implements KeyValueService {
4
+ #keyValueData: Map<string, string> | undefined;
5
+ #dirty = false;
6
+
7
+ public setKeyValueData(keyValueData: Map<string, string>) {
8
+ if (this.#keyValueData) {
9
+ throw new Error(
10
+ "Attempt to overwrite key-value data, it should be cleared first",
11
+ );
12
+ }
13
+ this.#keyValueData = keyValueData;
14
+ this.#dirty = false;
15
+ }
16
+
17
+ public clearKeyValueData() {
18
+ this.#keyValueData = undefined;
19
+ this.#dirty = false;
20
+ }
21
+
22
+ public isDirty(): boolean {
23
+ return this.#dirty;
24
+ }
25
+
26
+ public getKey(key: string): string {
27
+ if (this.#keyValueData === undefined) {
28
+ throw new Error("Attempt to access undefined key-value data");
29
+ }
30
+ const value = this.#keyValueData.get(key);
31
+ if (value === undefined) {
32
+ throw new Error("Attempt to access unknown key");
33
+ }
34
+
35
+ return value;
36
+ }
37
+
38
+ public hasKey(key: string): boolean {
39
+ if (this.#keyValueData === undefined) {
40
+ throw new Error("Attempt to access undefined key-value data");
41
+ }
42
+ return this.#keyValueData.has(key);
43
+ }
44
+
45
+ public setKey(key: string, value: string): void {
46
+ if (this.#keyValueData === undefined) {
47
+ throw new Error("Attempt to access undefined key-value data");
48
+ }
49
+ this.#keyValueData.set(key, value);
50
+ this.#dirty = true;
51
+ }
52
+
53
+ public deleteKey(key: string): void {
54
+ if (this.#keyValueData === undefined) {
55
+ throw new Error("Attempt to access undefined key-value data");
56
+ }
57
+ this.#keyValueData.delete(key);
58
+ this.#dirty = true;
59
+ }
60
+ }
@@ -0,0 +1,43 @@
1
+ import type GlobalModifierCommand from "../../../api/command/GlobalModifierCommand.ts";
2
+ import type GlobalCommandArgument from "../../../api/argument/GlobalCommandArgument.ts";
3
+ import {
4
+ type ArgumentSingleValueType,
5
+ ArgumentValueTypeName,
6
+ } from "../../../api/argument/ArgumentValueTypes.ts";
7
+ import type Context from "../../../api/Context.ts";
8
+ import type ConfigurationServiceProvider from "../ConfigurationServiceProvider.ts";
9
+
10
+ /**
11
+ * Command allowing the specification of the configuration file location used by the {@link ConfigurationServiceProvider}.
12
+ */
13
+ export default class ConfigCommand implements GlobalModifierCommand {
14
+ readonly name = "config";
15
+ readonly description = "Set the configuration file location";
16
+ readonly enableConfiguration = true;
17
+ readonly argument: GlobalCommandArgument = {
18
+ type: ArgumentValueTypeName.STRING,
19
+ configurationKey: "CONFIG",
20
+ };
21
+ readonly executePriority: number;
22
+
23
+ readonly #configurationServiceProvider: ConfigurationServiceProvider;
24
+
25
+ public constructor(
26
+ configurationServiceProvider: ConfigurationServiceProvider,
27
+ executePriority: number,
28
+ ) {
29
+ this.#configurationServiceProvider = configurationServiceProvider;
30
+ this.executePriority = executePriority;
31
+ }
32
+
33
+ public execute(
34
+ _context: Context,
35
+ argumentValue: ArgumentSingleValueType,
36
+ ): Promise<void> {
37
+ const configLocation = argumentValue as string;
38
+
39
+ this.#configurationServiceProvider.setConfigLocation(configLocation);
40
+
41
+ return Promise.resolve();
42
+ }
43
+ }