@flowscripter/dynamic-cli-framework 6.0.1 → 7.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 (124) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +10 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +7 -2
  5. package/dist/src/cli/BaseCLI.d.ts +8 -0
  6. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  7. package/dist/src/cli/BaseCLI.js +37 -4
  8. package/dist/src/launcher.d.ts +3 -3
  9. package/dist/src/launcher.d.ts.map +1 -1
  10. package/dist/src/launcher.js +6 -4
  11. package/dist/src/runtime/lifecycle/PriorityTaskList.d.ts +22 -0
  12. package/dist/src/runtime/lifecycle/PriorityTaskList.d.ts.map +1 -0
  13. package/dist/src/runtime/lifecycle/PriorityTaskList.js +23 -0
  14. package/dist/src/runtime/runner.d.ts +15 -7
  15. package/dist/src/runtime/runner.d.ts.map +1 -1
  16. package/dist/src/runtime/runner.js +82 -53
  17. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +26 -82
  18. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  19. package/dist/src/service/configuration/ConfigurationServiceProvider.js +68 -163
  20. package/dist/src/service/configuration/DefaultKeyValueService.d.ts +6 -3
  21. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  22. package/dist/src/service/configuration/DefaultKeyValueService.js +7 -24
  23. package/dist/src/service/configuration/DefaultSecretService.d.ts +1 -3
  24. package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
  25. package/dist/src/service/configuration/DefaultSecretService.js +2 -10
  26. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +105 -0
  27. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -0
  28. package/dist/src/service/configuration/KeyValueServiceProvider.js +201 -0
  29. package/dist/src/service/configuration/command/ConfigCommand.d.ts +4 -5
  30. package/dist/src/service/configuration/command/ConfigCommand.d.ts.map +1 -1
  31. package/dist/src/service/configuration/command/ConfigCommand.js +7 -6
  32. package/dist/src/service/configuration/command/DumpConfigCommand.d.ts +7 -3
  33. package/dist/src/service/configuration/command/DumpConfigCommand.d.ts.map +1 -1
  34. package/dist/src/service/configuration/command/DumpConfigCommand.js +10 -5
  35. package/dist/src/service/fetch/DefaultFetchService.d.ts +2 -2
  36. package/dist/src/service/fetch/DefaultFetchService.d.ts.map +1 -1
  37. package/dist/src/service/fetch/DefaultFetchService.js +17 -12
  38. package/dist/src/service/fetch/FetchServiceProvider.d.ts.map +1 -1
  39. package/dist/src/service/fetch/FetchServiceProvider.js +2 -3
  40. package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
  41. package/dist/src/service/printer/PrinterServiceProvider.js +11 -7
  42. package/dist/src/service/printer/command/DarkModeCommand.d.ts +3 -5
  43. package/dist/src/service/printer/command/DarkModeCommand.d.ts.map +1 -1
  44. package/dist/src/service/printer/command/DarkModeCommand.js +6 -6
  45. package/dist/src/service/printer/command/LogLevelCommand.d.ts +3 -5
  46. package/dist/src/service/printer/command/LogLevelCommand.d.ts.map +1 -1
  47. package/dist/src/service/printer/command/LogLevelCommand.js +9 -9
  48. package/dist/src/service/printer/command/NoColorCommand.d.ts +3 -5
  49. package/dist/src/service/printer/command/NoColorCommand.d.ts.map +1 -1
  50. package/dist/src/service/printer/command/NoColorCommand.js +6 -6
  51. package/dist/src/service/printer/command/NoHyperlinksCommand.d.ts +2 -4
  52. package/dist/src/service/printer/command/NoHyperlinksCommand.d.ts.map +1 -1
  53. package/dist/src/service/printer/command/NoHyperlinksCommand.js +5 -5
  54. package/dist/src/service/prompter/PrompterServiceProvider.js +1 -1
  55. package/dist/src/service/prompter/command/NoPromptCommand.d.ts +2 -4
  56. package/dist/src/service/prompter/command/NoPromptCommand.d.ts.map +1 -1
  57. package/dist/src/service/prompter/command/NoPromptCommand.js +5 -5
  58. package/dist/src/service/shutdown/DefaultShutdownService.d.ts +4 -3
  59. package/dist/src/service/shutdown/DefaultShutdownService.d.ts.map +1 -1
  60. package/dist/src/service/shutdown/DefaultShutdownService.js +4 -3
  61. package/dist/src/service/shutdown/ShutdownServiceProvider.js +2 -2
  62. package/dist/src/service/spawn/DefaultSpawnService.d.ts +2 -2
  63. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
  64. package/dist/src/service/spawn/DefaultSpawnService.js +17 -14
  65. package/dist/src/service/spawn/SpawnServiceProvider.d.ts.map +1 -1
  66. package/dist/src/service/spawn/SpawnServiceProvider.js +1 -4
  67. package/dist/src/service/startup/DefaultStartupService.d.ts +10 -0
  68. package/dist/src/service/startup/DefaultStartupService.d.ts.map +1 -0
  69. package/dist/src/service/startup/DefaultStartupService.js +13 -0
  70. package/dist/src/service/startup/StartupServiceProvider.d.ts +17 -0
  71. package/dist/src/service/startup/StartupServiceProvider.d.ts.map +1 -0
  72. package/dist/src/service/startup/StartupServiceProvider.js +26 -0
  73. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +10 -4
  74. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
  75. package/dist/src/service/upgrade/DefaultUpgradeService.js +156 -37
  76. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts +10 -0
  77. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -1
  78. package/dist/src/service/upgrade/UpgradeServiceProvider.js +25 -6
  79. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts +0 -3
  80. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -1
  81. package/dist/src/service/upgrade/command/UpgradeSubCommand.js +5 -13
  82. package/dist/src/startup/banner/bannerStartupTask.d.ts +17 -0
  83. package/dist/src/startup/banner/bannerStartupTask.d.ts.map +1 -0
  84. package/dist/src/startup/banner/bannerStartupTask.js +66 -0
  85. package/dist/src/{service → startup}/banner/command/NoBannerCommand.d.ts +8 -2
  86. package/dist/src/startup/banner/command/NoBannerCommand.d.ts.map +1 -0
  87. package/dist/src/{service → startup}/banner/command/NoBannerCommand.js +4 -4
  88. package/index.ts +19 -3
  89. package/package.json +2 -2
  90. package/src/cli/BaseCLI.ts +49 -3
  91. package/src/launcher.ts +7 -2
  92. package/src/runtime/lifecycle/PriorityTaskList.ts +34 -0
  93. package/src/runtime/runner.ts +106 -67
  94. package/src/service/configuration/ConfigurationServiceProvider.ts +80 -180
  95. package/src/service/configuration/DefaultKeyValueService.ts +8 -27
  96. package/src/service/configuration/DefaultSecretService.ts +3 -13
  97. package/src/service/configuration/KeyValueServiceProvider.ts +246 -0
  98. package/src/service/configuration/command/ConfigCommand.ts +12 -11
  99. package/src/service/configuration/command/DumpConfigCommand.ts +10 -9
  100. package/src/service/fetch/DefaultFetchService.ts +18 -12
  101. package/src/service/fetch/FetchServiceProvider.ts +2 -4
  102. package/src/service/printer/PrinterServiceProvider.ts +11 -7
  103. package/src/service/printer/command/DarkModeCommand.ts +7 -8
  104. package/src/service/printer/command/LogLevelCommand.ts +10 -11
  105. package/src/service/printer/command/NoColorCommand.ts +7 -8
  106. package/src/service/printer/command/NoHyperlinksCommand.ts +6 -7
  107. package/src/service/prompter/PrompterServiceProvider.ts +1 -1
  108. package/src/service/prompter/command/NoPromptCommand.ts +6 -7
  109. package/src/service/shutdown/DefaultShutdownService.ts +5 -4
  110. package/src/service/shutdown/ShutdownServiceProvider.ts +2 -2
  111. package/src/service/spawn/DefaultSpawnService.ts +18 -14
  112. package/src/service/spawn/SpawnServiceProvider.ts +1 -5
  113. package/src/service/startup/DefaultStartupService.ts +17 -0
  114. package/src/service/startup/StartupServiceProvider.ts +36 -0
  115. package/src/service/upgrade/DefaultUpgradeService.ts +202 -42
  116. package/src/service/upgrade/UpgradeServiceProvider.ts +31 -8
  117. package/src/service/upgrade/command/UpgradeSubCommand.ts +5 -15
  118. package/src/startup/banner/bannerStartupTask.ts +96 -0
  119. package/src/{service → startup}/banner/command/NoBannerCommand.ts +12 -5
  120. package/dist/src/service/banner/BannerServiceProvider.d.ts +0 -29
  121. package/dist/src/service/banner/BannerServiceProvider.d.ts.map +0 -1
  122. package/dist/src/service/banner/BannerServiceProvider.js +0 -69
  123. package/dist/src/service/banner/command/NoBannerCommand.d.ts.map +0 -1
  124. package/src/service/banner/BannerServiceProvider.ts +0 -89
@@ -15,10 +15,10 @@ import getLogger from "../../util/logger.ts";
15
15
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
16
16
  import DumpConfigCommand from "./command/DumpConfigCommand.ts";
17
17
  import {
18
- KEY_VALUE_SERVICE_ID,
18
+ CONFIGURATION_SERVICE_ID,
19
19
  SECRET_SENTINEL_PREFIX,
20
20
  } from "@flowscripter/dynamic-cli-framework-api";
21
- import DefaultKeyValueService from "./DefaultKeyValueService.ts";
21
+ import type { ConfigurationService } from "@flowscripter/dynamic-cli-framework-api";
22
22
  import DefaultSecretService from "./DefaultSecretService.ts";
23
23
  import resolveSecrets from "./resolveSecrets.ts";
24
24
  import type { Command } from "@flowscripter/dynamic-cli-framework-api";
@@ -36,11 +36,35 @@ import {
36
36
 
37
37
  const logger = getLogger("ConfigurationServiceProvider");
38
38
 
39
+ export type KeyValueServiceScopeType = "command" | "service";
40
+
41
+ class ConfigurationServiceImpl implements ConfigurationService {
42
+ readonly #provider: ConfigurationServiceProvider;
43
+
44
+ public constructor(provider: ConfigurationServiceProvider) {
45
+ this.#provider = provider;
46
+ }
47
+
48
+ public get configLocation(): string | undefined {
49
+ return this.#provider.configLocation;
50
+ }
51
+
52
+ public setConfigLocation(location: string): void {
53
+ this.#provider.setConfigLocation(location);
54
+ }
55
+ }
56
+
39
57
  /**
40
- * Provides:
58
+ * Provides configuration of default command arguments using a configuration file and/or
59
+ * environment variables.
41
60
  *
42
- * * Configuration of default command arguments using a configuration file and/or environment variables.
43
- * * A generic key-value store for commands and services ({@link KeyValueService}).
61
+ * The same configuration file also backs the raw, per-scope key-value data consumed by
62
+ * {@link KeyValueServiceProvider} (which holds a direct reference to this provider - see
63
+ * {@link getKeyValueData} and {@link flushIfDirty} - rather than looking this provider up via
64
+ * {@link Context}, since only {@link configLocation}/{@link setConfigLocation} are safe to expose
65
+ * generally; {@link getConfigString} is wired directly to {@link DumpConfigCommand} for the same
66
+ * reason - a full config dump would leak every other command's/service's key-value data if it
67
+ * were reachable through a general lookup).
44
68
  *
45
69
  * **Default Command Arguments**
46
70
  *
@@ -85,7 +109,8 @@ const logger = getLogger("ConfigurationServiceProvider");
85
109
  * NOTE: You may store default arguments for secrets by manually configuring secrets in your OS
86
110
  * credential store and referencing them in the config file using the sentinel format
87
111
  * `__SECRET__:<bun_secret_name>`. These values will be resolved from the OS secret store
88
- * before being returned as default argument values. See "Generic Key-Value Service" below.
112
+ * before being returned as default argument values. See {@link KeyValueServiceProvider} for the
113
+ * generic key-value store which uses the same secret sentinel mechanism.
89
114
  *
90
115
  * *Environment Variables*
91
116
  *
@@ -123,68 +148,13 @@ const logger = getLogger("ConfigurationServiceProvider");
123
148
  * * 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`
124
149
  *
125
150
  * NOTE: Any default values from the above configuration sources will be overridden by any arguments provided on the command line.
126
- *
127
- * **Generic Key-Value Service**
128
- *
129
- * The same configuration file above is used to provide storage for the provided {@link KeyValueService}. The values
130
- * are stored under a top level `key-values` property. These are expected to not be modified by the user of the CLI.
131
- *
132
- * The second and third level of properties is used to scope the key-values to specific command names
133
- * (via {@link Command.name} values) and specific service IDs (via {@link ServiceProvider.serviceId} values).
134
- * Keys are strings, but values are arbitrary JSON (see {@link ValueNode}) - not limited to strings -
135
- * and may be deep objects or arrays.
136
- *
137
- * Any node within a value passed to {@link KeyValueService.set} can be wrapped in {@link Secret} -
138
- * at any depth - to have that node, and only that node, stored as an OS-native secret. When a node
139
- * is wrapped in {@link Secret}, its value (of any shape) is JSON-serialized and stored as a single
140
- * OS-native secret via Bun.secrets, with a sentinel value of the format `__SECRET__:<bun_secret_name>`
141
- * substituted in its place in the structure that gets stored in the config file. Everything else in
142
- * the value is stored as plain (unencrypted) config data. The sentinel prefix `__SECRET__:` is
143
- * reserved and must not be used for regular key-value data.
144
- *
145
- * Independently of how a value was written, {@link KeyValueService.get} recursively resolves any
146
- * string leaf - at any depth within the retrieved value - which starts with the sentinel prefix,
147
- * via the OS secret store. This means a secret reference may also be hand-embedded (nested
148
- * arbitrarily deep) directly within a plain, non-secret value in the config file.
149
- *
150
- * Secret support requires `secretServiceEnabled=true` in the constructor (which also requires
151
- * `configEnabled=true`).
152
- *
153
- * As an example:
154
- * ```
155
- * {
156
- * "defaults": {
157
- * ...
158
- * },
159
- * "key-values": {
160
- * "commands": {
161
- * "command1": {
162
- * "foo1": "bar1",
163
- * "foo2": "__SECRET__:command_command1_foo2",
164
- * "foo3": {
165
- * "nested": ["a", "__SECRET__:command_command1_foo3_nested"]
166
- * }
167
- * },
168
- * "command2": {
169
- * "foo1": "bar3"
170
- * }
171
- * },
172
- * "services": {
173
- * "service-id-1": {
174
- * "foo1": "bar"
175
- * }
176
- * }
177
- * }
178
- * }
179
- * ```
180
151
  */
181
152
  export default class ConfigurationServiceProvider implements ServiceProvider {
182
- readonly serviceId: string = KEY_VALUE_SERVICE_ID;
153
+ readonly serviceId: string = CONFIGURATION_SERVICE_ID;
183
154
  readonly servicePriority: number;
184
155
 
185
156
  public readonly envVarsEnabled: boolean;
186
157
  public readonly configEnabled: boolean;
187
- public readonly keyValueServiceEnabled: boolean;
188
158
  public readonly secretServiceEnabled: boolean;
189
159
 
190
160
  // the location of the currently managed configuration data
@@ -195,22 +165,24 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
195
165
  // SubCommandArgument values.
196
166
  public defaultsData: Map<string, Values | SingleValueType> = new Map();
197
167
 
198
- // the configuration data to be used by the CLI runner implementation
199
- // when updating command scoped access to key-value data via the key-value service.
168
+ // the underlying per-scope key-value data, read from/written to the configuration file. Exposed
169
+ // (raw, not wrapped in a KeyValueService) only via getKeyValueData(), for KeyValueServiceProvider's
170
+ // exclusive use.
200
171
  #commandKeyValueData = new Map<string, Map<string, ValueNode>>();
201
-
202
- // the configuration data to be used by the CLI runner implementation
203
- // when updating service scoped access to key-value data via the key-value service.
204
172
  #serviceKeyValueData = new Map<string, Map<string, ValueNode>>();
205
173
 
206
- // the optional service providing scope limited key-value data to commands and other services
207
- #defaultKeyValueService: DefaultKeyValueService | undefined;
208
-
209
- // the optional service providing OS-native secret storage
210
- #defaultSecretService: DefaultSecretService | undefined;
174
+ // secret service used only to resolve secret sentinels embedded in default command argument
175
+ // values (getDefaultArgumentValues()) - unrelated to per-command/per-service KV scoping, so it
176
+ // uses its own fixed, dedicated scope prefix ("defaults"). Its scope is otherwise irrelevant,
177
+ // since only getSecret() (which doesn't consult scope) is ever called on it.
178
+ #defaultsSecretService: DefaultSecretService | undefined;
211
179
 
212
- #currentCommandNameKeyValueScope: string | undefined;
213
- #currentServiceIdKeyValueScope: string | undefined;
180
+ /**
181
+ * A {@link ConfigurationService} view of this provider, registered in the {@link Context} under
182
+ * {@link CONFIGURATION_SERVICE_ID}. Its `configLocation` getter always reflects this provider's
183
+ * current value.
184
+ */
185
+ readonly #configurationService: ConfigurationService;
214
186
 
215
187
  /**
216
188
  * Create an instance of the service provider with the specified details.
@@ -218,45 +190,38 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
218
190
  * @param servicePriority the priority of the service.
219
191
  * @param envVarsEnabled optionally support checking env variables for default argument values.
220
192
  * @param configEnabled optionally enable configuration file support for default argument values.
221
- * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configEnabled` must be true in this case
222
- * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets: `configEnabled` must be true in this case
193
+ * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets for resolving
194
+ * secrets embedded in default argument values: `configEnabled` must be true in this case
223
195
  */
224
196
  public constructor(
225
197
  servicePriority: number,
226
198
  envVarsEnabled = false,
227
199
  configEnabled = false,
228
- keyValueServiceEnabled = false,
229
200
  secretServiceEnabled = false,
230
201
  ) {
231
- if (!configEnabled && keyValueServiceEnabled) {
232
- throw new Error("configEnabled must be true if keyValueServiceEnabled is true");
233
- }
234
202
  if (!configEnabled && secretServiceEnabled) {
235
203
  throw new Error("configEnabled must be true if secretServiceEnabled is true");
236
204
  }
237
205
  this.servicePriority = servicePriority;
238
206
  this.envVarsEnabled = envVarsEnabled;
239
207
  this.configEnabled = configEnabled;
240
- this.keyValueServiceEnabled = keyValueServiceEnabled;
241
208
  this.secretServiceEnabled = secretServiceEnabled;
209
+
210
+ this.#configurationService = new ConfigurationServiceImpl(this);
242
211
  }
243
212
 
244
213
  public getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo> {
245
214
  const commands: Array<Command> = [];
246
215
 
247
216
  if (this.configEnabled) {
248
- commands.push(new ConfigCommand(this, this.servicePriority));
249
- commands.push(new DumpConfigCommand(this));
217
+ commands.push(new ConfigCommand(this.servicePriority));
218
+ commands.push(new DumpConfigCommand(() => this.getConfigString()));
250
219
  }
251
220
  if (this.secretServiceEnabled) {
252
- this.#defaultSecretService = new DefaultSecretService(cliConfig.name);
253
- }
254
- if (this.keyValueServiceEnabled || this.secretServiceEnabled) {
255
- this.#defaultKeyValueService = new DefaultKeyValueService(this.#defaultSecretService);
221
+ this.#defaultsSecretService = new DefaultSecretService(cliConfig.name, "defaults");
256
222
  }
257
223
  return Promise.resolve({
258
- // this may be undefined if keyValueServiceEnabled and secretServiceEnabled are false
259
- service: this.#defaultKeyValueService,
224
+ service: this.#configurationService,
260
225
  // this may be empty if configEnabled is false
261
226
  commands,
262
227
  });
@@ -313,11 +278,11 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
313
278
  }
314
279
  }
315
280
 
316
- if (result !== undefined && this.#defaultSecretService) {
281
+ if (result !== undefined && this.#defaultsSecretService) {
317
282
  result = await resolveSecrets<PopulatedValues | PopulatedValueType>(
318
283
  result,
319
284
  async (bunSecretName) => {
320
- const secretValue = await this.#defaultSecretService!.getSecret(bunSecretName);
285
+ const secretValue = await this.#defaultsSecretService!.getSecret(bunSecretName);
321
286
  if (secretValue === null) {
322
287
  throw new Error(
323
288
  `Secret not found in OS secret store for sentinel: '${SECRET_SENTINEL_PREFIX}${bunSecretName}'`,
@@ -332,104 +297,39 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
332
297
  }
333
298
 
334
299
  /**
335
- * Indicate that the contained {@link DefaultKeyValueService} should be set to have a scope of the provided
336
- * command name.
337
- *
338
- * @param commandName the name of the command to scope the key-value data to.
339
- *
340
- * @throws {Error} if an existing scope is set.
341
- */
342
- public setCommandKeyValueScope(commandName: string): void {
343
- if (!this.keyValueServiceEnabled && !this.secretServiceEnabled) {
344
- throw new Error(`Attempt to use KeyValueService/SecretService which is not enabled`);
345
- }
346
- if (this.#currentCommandNameKeyValueScope) {
347
- throw new Error(
348
- `Attempt to set CommandNameKeyValueScope to ${commandName} when it is currently set to ${this.#currentCommandNameKeyValueScope}`,
349
- );
350
- }
351
- if (this.#currentServiceIdKeyValueScope) {
352
- throw new Error(
353
- `Attempt to set CommandNameKeyValueScope to ${commandName} when ServiceIdKeyValueScope is currently set to ${this.#currentCommandNameKeyValueScope}`,
354
- );
355
- }
356
- this.#currentCommandNameKeyValueScope = commandName;
357
-
358
- logger.debug("currentCommandNameKeyValueScope: %s", commandName);
359
-
360
- if (this.#defaultKeyValueService) {
361
- if (!this.#commandKeyValueData.has(commandName)) {
362
- this.#commandKeyValueData.set(commandName, new Map());
363
- }
364
- this.#defaultKeyValueService.setKeyValueData(this.#commandKeyValueData.get(commandName)!);
365
- }
366
- if (this.#defaultSecretService) {
367
- this.#defaultSecretService.setScope("command_" + commandName);
368
- }
369
- }
370
-
371
- /**
372
- * Indicate that the contained {@link DefaultKeyValueService} should be set to have a scope of the provided
373
- * service ID.
374
- *
375
- * @param serviceId the ID of the service to scope the key-value data to.
300
+ * Get-or-create the underlying, raw {@link ValueNode} `Map` for the given scope - for
301
+ * {@link KeyValueServiceProvider}'s exclusive use, via its direct reference to this provider.
376
302
  *
377
- * @throws {Error} if an existing scope is set.
303
+ * @param scopeType whether `scopeKey` is a command name or a service/task ID.
304
+ * @param scopeKey the command name or service/task ID to scope the key-value data to.
378
305
  */
379
- public setServiceKeyValueScope(serviceId: string): void {
380
- if (!this.keyValueServiceEnabled && !this.secretServiceEnabled) {
381
- throw new Error(`Attempt to use KeyValueService/SecretService which is not enabled`);
382
- }
383
- if (this.#currentServiceIdKeyValueScope) {
384
- throw new Error(
385
- `Attempt to set ServiceIdKeyValueScope to ${serviceId} when it is currently set to ${this.#currentServiceIdKeyValueScope}`,
386
- );
387
- }
388
- if (this.#currentCommandNameKeyValueScope) {
389
- throw new Error(
390
- `Attempt to set ServiceIdKeyValueScope to ${serviceId} when CommandNameKeyValueScope is currently set to ${this.#currentServiceIdKeyValueScope}`,
391
- );
392
- }
393
- this.#currentServiceIdKeyValueScope = serviceId;
394
-
395
- logger.debug("currentServiceIdKeyValueScope: %s", serviceId);
396
-
397
- if (this.#defaultKeyValueService) {
398
- if (!this.#serviceKeyValueData.has(serviceId)) {
399
- this.#serviceKeyValueData.set(serviceId, new Map());
400
- }
401
- this.#defaultKeyValueService.setKeyValueData(this.#serviceKeyValueData.get(serviceId)!);
402
- }
403
- if (this.#defaultSecretService) {
404
- this.#defaultSecretService.setScope("service_" + serviceId);
405
- }
306
+ public getKeyValueData(
307
+ scopeType: KeyValueServiceScopeType,
308
+ scopeKey: string,
309
+ ): Map<string, ValueNode> {
310
+ const keyValueData =
311
+ scopeType === "command" ? this.#commandKeyValueData : this.#serviceKeyValueData;
312
+ if (!keyValueData.has(scopeKey)) {
313
+ keyValueData.set(scopeKey, new Map());
314
+ }
315
+ return keyValueData.get(scopeKey)!;
406
316
  }
407
317
 
408
318
  /**
409
- * Indicate that the contained {@link DefaultKeyValueService} should have any current scope cleared.
319
+ * Write the configuration file, if `isDirty` is true - for {@link KeyValueServiceProvider}'s
320
+ * exclusive use, via its direct reference to this provider, called from its shutdown-time
321
+ * flush task.
410
322
  *
411
- * If a scope is currently set and changes have been made to the key-value data, they will be written
412
- * to the current configuration location.
323
+ * @param isDirty whether any scoped KeyValueService has unwritten changes.
413
324
  */
414
- public async clearKeyValueScope(): Promise<void> {
415
- if (!this.keyValueServiceEnabled && !this.secretServiceEnabled) {
416
- throw new Error(`Attempt to use KeyValueService/SecretService which is not enabled`);
417
- }
418
- if (this.#defaultKeyValueService && this.#defaultKeyValueService.isDirty()) {
419
- if (this.configLocation === undefined) {
420
- throw new Error("Attempt to write updated config with no configLocation set");
421
- }
422
- await this.#writeConfig(this.configLocation);
423
- }
424
- this.#currentCommandNameKeyValueScope = undefined;
425
- this.#currentServiceIdKeyValueScope = undefined;
426
-
427
- if (this.#defaultKeyValueService) {
428
- this.#defaultKeyValueService.clearKeyValueData();
325
+ public async flushIfDirty(isDirty: boolean): Promise<void> {
326
+ if (!isDirty) {
327
+ return;
429
328
  }
430
- if (this.#defaultSecretService) {
431
- this.#defaultSecretService.clearScope();
329
+ if (this.configLocation === undefined) {
330
+ throw new Error("Attempt to write updated config with no configLocation set");
432
331
  }
332
+ await this.#writeConfig(this.configLocation);
433
333
  }
434
334
 
435
335
  public async initService(context: Context): Promise<void> {
@@ -7,26 +7,19 @@ import { Secret, SECRET_SENTINEL_PREFIX } from "@flowscripter/dynamic-cli-framew
7
7
  import type DefaultSecretService from "./DefaultSecretService.ts";
8
8
  import resolveSecrets from "./resolveSecrets.ts";
9
9
 
10
+ /**
11
+ * Bound permanently to a single scope's data at construction - never re-pointed afterward. Create
12
+ * a separate instance per scope (see {@link KeyValueServiceProvider.getScopedKeyValueService}),
13
+ * rather than sharing one instance across scopes.
14
+ */
10
15
  export default class DefaultKeyValueService implements KeyValueService {
11
- #keyValueData: Map<string, ValueNode> | undefined;
16
+ readonly #keyValueData: Map<string, ValueNode>;
12
17
  #dirty = false;
13
18
  readonly #secretService: DefaultSecretService | undefined;
14
19
 
15
- constructor(secretService?: DefaultSecretService) {
16
- this.#secretService = secretService;
17
- }
18
-
19
- public setKeyValueData(keyValueData: Map<string, ValueNode>) {
20
- if (this.#keyValueData) {
21
- throw new Error("Attempt to overwrite key-value data, it should be cleared first");
22
- }
20
+ constructor(keyValueData: Map<string, ValueNode>, secretService?: DefaultSecretService) {
23
21
  this.#keyValueData = keyValueData;
24
- this.#dirty = false;
25
- }
26
-
27
- public clearKeyValueData() {
28
- this.#keyValueData = undefined;
29
- this.#dirty = false;
22
+ this.#secretService = secretService;
30
23
  }
31
24
 
32
25
  public isDirty(): boolean {
@@ -34,9 +27,6 @@ export default class DefaultKeyValueService implements KeyValueService {
34
27
  }
35
28
 
36
29
  public async get<T extends ValueNode = ValueNode>(key: string): Promise<T> {
37
- if (this.#keyValueData === undefined) {
38
- throw new Error("Attempt to access undefined key-value data");
39
- }
40
30
  const value = this.#keyValueData.get(key);
41
31
  if (value === undefined) {
42
32
  throw new Error("Attempt to access unknown key");
@@ -61,16 +51,10 @@ export default class DefaultKeyValueService implements KeyValueService {
61
51
  }
62
52
 
63
53
  public has(key: string): Promise<boolean> {
64
- if (this.#keyValueData === undefined) {
65
- return Promise.reject(new Error("Attempt to access undefined key-value data"));
66
- }
67
54
  return Promise.resolve(this.#keyValueData.has(key));
68
55
  }
69
56
 
70
57
  public async set(key: string, value: SettableValueNode): Promise<void> {
71
- if (this.#keyValueData === undefined) {
72
- throw new Error("Attempt to access undefined key-value data");
73
- }
74
58
  this.#keyValueData.set(key, await this.#storeSecrets(key, value, []));
75
59
  this.#dirty = true;
76
60
  }
@@ -112,9 +96,6 @@ export default class DefaultKeyValueService implements KeyValueService {
112
96
  }
113
97
 
114
98
  public async delete(key: string): Promise<void> {
115
- if (this.#keyValueData === undefined) {
116
- throw new Error("Attempt to access undefined key-value data");
117
- }
118
99
  const value = this.#keyValueData.get(key);
119
100
  if (value !== undefined) {
120
101
  await this.#deleteSecrets(value);
@@ -13,15 +13,16 @@ export interface SecretsApi {
13
13
  export default class DefaultSecretService implements SecretService {
14
14
  readonly #serviceName: string;
15
15
  readonly #secrets: SecretsApi;
16
- #scope: string | undefined;
16
+ readonly #scope: string;
17
17
 
18
- constructor(serviceName: string, secretsApi?: SecretsApi) {
18
+ constructor(serviceName: string, scope: string, secretsApi?: SecretsApi) {
19
19
  this.#serviceName = DefaultSecretService.#sanitize(serviceName);
20
20
  if (this.#serviceName.length > MAX_NAME_LENGTH) {
21
21
  throw new Error(
22
22
  `Service name exceeds ${MAX_NAME_LENGTH} characters after sanitization: '${this.#serviceName}'`,
23
23
  );
24
24
  }
25
+ this.#scope = DefaultSecretService.#sanitize(scope);
25
26
  this.#secrets = secretsApi ?? bunSecrets;
26
27
  }
27
28
 
@@ -29,18 +30,7 @@ export default class DefaultSecretService implements SecretService {
29
30
  return value.replace(/[^a-zA-Z0-9_]/g, "_");
30
31
  }
31
32
 
32
- public setScope(scope: string): void {
33
- this.#scope = DefaultSecretService.#sanitize(scope);
34
- }
35
-
36
- public clearScope(): void {
37
- this.#scope = undefined;
38
- }
39
-
40
33
  public async setSecret(key: string, value: string): Promise<string> {
41
- if (this.#scope === undefined) {
42
- throw new Error("Attempt to set secret without a scope being set");
43
- }
44
34
  const name = this.#scope + "_" + DefaultSecretService.#sanitize(key);
45
35
  if (name.length > MAX_NAME_LENGTH) {
46
36
  throw new Error(`Secret name exceeds ${MAX_NAME_LENGTH} characters: '${name}'`);