@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "6.0.1",
3
+ "version": "7.0.0",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -48,7 +48,7 @@
48
48
  "build": "tsc -p tsconfig.build.json"
49
49
  },
50
50
  "dependencies": {
51
- "@flowscripter/dynamic-cli-framework-api": "^3.0.1",
51
+ "@flowscripter/dynamic-cli-framework-api": "^4.0.1",
52
52
  "@flowscripter/dynamic-plugin-framework": "^2.2.9",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
@@ -28,8 +28,11 @@ import {
28
28
  } from "../runtime/command/CommandTypeGuards.ts";
29
29
  import CommandValidator from "../runtime/command/CommandValidator.ts";
30
30
  import ShutdownServiceProvider from "../service/shutdown/ShutdownServiceProvider.ts";
31
+ import StartupServiceProvider from "../service/startup/StartupServiceProvider.ts";
32
+ import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
31
33
  import { shutdownState } from "../service/shutdown/ShutdownState.ts";
32
34
  import ConfigurationServiceProvider from "../service/configuration/ConfigurationServiceProvider.ts";
35
+ import KeyValueServiceProvider from "../service/configuration/KeyValueServiceProvider.ts";
33
36
  import PrinterServiceProvider from "../service/printer/PrinterServiceProvider.ts";
34
37
  import TableGeneratorServiceProvider from "../service/tableGenerator/TableGeneratorServiceProvider.ts";
35
38
  import { run } from "../runtime/runner.ts";
@@ -53,7 +56,9 @@ import CompletionServiceProvider from "../service/completion/CompletionServicePr
53
56
  import ImagePrinterServiceProvider from "../service/imagePrinter/ImagePrinterServiceProvider.ts";
54
57
  import SpawnServiceProvider from "../service/spawn/SpawnServiceProvider.ts";
55
58
  import FetchServiceProvider from "../service/fetch/FetchServiceProvider.ts";
56
- import UpgradeServiceProvider from "../service/upgrade/UpgradeServiceProvider.ts";
59
+ import UpgradeServiceProvider, {
60
+ createUpgradeCheckStartupTask,
61
+ } from "../service/upgrade/UpgradeServiceProvider.ts";
57
62
  import PluginServiceProvider from "../service/plugin/PluginServiceProvider.ts";
58
63
  const logger = getLogger("BaseCLI");
59
64
 
@@ -103,6 +108,7 @@ export default class BaseCLI implements CLI {
103
108
  readonly #context: DefaultContext;
104
109
  readonly #printerService: PrinterService;
105
110
  readonly #stdoutTerminal: Terminal;
111
+ readonly #startupServiceProvider: StartupServiceProvider;
106
112
 
107
113
  constructor(
108
114
  cliConfig: CLIConfig,
@@ -168,6 +174,10 @@ export default class BaseCLI implements CLI {
168
174
  // create a context
169
175
  this.#context = new DefaultContext(this.#cliConfig);
170
176
 
177
+ // priority 95 sits between shutdown (100) and configuration (90) - it has no ordering
178
+ // dependency on anything since its own initService() is a no-op.
179
+ this.#startupServiceProvider = new StartupServiceProvider(95);
180
+
171
181
  this.#stdoutTerminal = stdoutTerminal;
172
182
  this.#printerService = new DefaultPrinterService(
173
183
  stdoutWritableStream,
@@ -201,6 +211,16 @@ export default class BaseCLI implements CLI {
201
211
  *
202
212
  * @param command the {@link Command} to add.
203
213
  */
214
+ /**
215
+ * Register a {@link StartupTask} to run once during CLI startup, in priority order alongside
216
+ * every registered {@link ServiceProvider}'s `initService()` call.
217
+ *
218
+ * @param task the {@link StartupTask} to register.
219
+ */
220
+ public addStartupTask(task: StartupTask) {
221
+ this.#startupServiceProvider.startupService.registerTask(task);
222
+ }
223
+
204
224
  public addCommand(command: Command) {
205
225
  if (!isGlobalModifierCommand(command)) {
206
226
  // store the command locally to help determine if this CLI will be a single or multi-command CLI
@@ -229,6 +249,7 @@ export default class BaseCLI implements CLI {
229
249
 
230
250
  // create and add core services
231
251
  this.addServiceProvider(new ShutdownServiceProvider(100));
252
+ this.addServiceProvider(this.#startupServiceProvider);
232
253
  this.addServiceProvider(new PrinterServiceProvider(80, this.#printerService));
233
254
  this.addServiceProvider(new TableGeneratorServiceProvider(70));
234
255
 
@@ -293,10 +314,12 @@ export default class BaseCLI implements CLI {
293
314
  );
294
315
  }
295
316
 
317
+ let upgradeServiceProvider: UpgradeServiceProvider | undefined;
296
318
  if (this.#options.upgradeServiceEnabled) {
297
319
  // 56 runs after Spawn(58)/Fetch(57) - whose dependencies it needs
298
320
  // but before the consumer-configured Banner/Plugin(50) priority band.
299
- this.addServiceProvider(new UpgradeServiceProvider(56, this.#options.upgradeLocationsConfig));
321
+ upgradeServiceProvider = new UpgradeServiceProvider(56, this.#options.upgradeLocationsConfig);
322
+ this.addServiceProvider(upgradeServiceProvider);
300
323
  }
301
324
 
302
325
  if (this.#options.pluginServiceEnabled) {
@@ -315,10 +338,18 @@ export default class BaseCLI implements CLI {
315
338
  90,
316
339
  this.#options.envVarsSupportEnabled,
317
340
  this.#options.configFileSupportEnabled,
318
- this.#options.keyValueServiceEnabled,
319
341
  this.#options.secretServiceEnabled,
320
342
  );
321
343
  this.addServiceProvider(configurationServiceProvider);
344
+ // priority 89 sits just below ConfigurationServiceProvider's 90 - its config-file read
345
+ // happens in its own initService(), before KeyValueServiceProvider's initService() runs.
346
+ const keyValueServiceProvider = new KeyValueServiceProvider(
347
+ 89,
348
+ configurationServiceProvider,
349
+ this.#options.keyValueServiceEnabled,
350
+ this.#options.secretServiceEnabled,
351
+ );
352
+ this.addServiceProvider(keyValueServiceProvider);
322
353
 
323
354
  for (const serviceProvider of this.#serviceProviderRegistry.getServiceProviders()) {
324
355
  const serviceInfo = await serviceProvider.getServiceInfo(this.#cliConfig);
@@ -332,6 +363,19 @@ export default class BaseCLI implements CLI {
332
363
  });
333
364
  }
334
365
 
366
+ if (upgradeServiceProvider?.upgradeService) {
367
+ this.addStartupTask(createUpgradeCheckStartupTask(upgradeServiceProvider.upgradeService, 56));
368
+ }
369
+
370
+ // directly-registered StartupTasks (e.g. the banner task) aren't backed by a ServiceProvider,
371
+ // so their modifierCommands need to be added to the command registry here instead, keyed by
372
+ // the task's own ID.
373
+ for (const task of this.#startupServiceProvider.startupService.getTasks()) {
374
+ (task.modifierCommands ?? []).forEach((command) => {
375
+ this.#commandRegistry.addCommand(command, task.id);
376
+ });
377
+ }
378
+
335
379
  logger.debug(() => `Running with args: ${args.join(" ")}`);
336
380
 
337
381
  let helpSubCommand: SubCommand | undefined;
@@ -381,8 +425,10 @@ export default class BaseCLI implements CLI {
381
425
  this.#commandRegistry,
382
426
  this.#serviceProviderRegistry,
383
427
  configurationServiceProvider,
428
+ keyValueServiceProvider,
384
429
  this.#context,
385
430
  defaultCommand,
431
+ this.#startupServiceProvider.startupService,
386
432
  );
387
433
  // then handle the result...
388
434
  if (runResult.runState === RunState.NO_COMMAND) {
package/src/launcher.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  Command,
7
7
  RunResult,
8
8
  ServiceProvider,
9
+ StartupTask,
9
10
  } from "@flowscripter/dynamic-cli-framework-api";
10
11
  import type BaseCLIFeatureOptions from "./cli/BaseCLIFeatureOptions.ts";
11
12
  import DefaultRuntimeCLI from "./cli/DefaultRuntimeCLI.ts";
@@ -17,6 +18,7 @@ export async function launchSingleCommandCLI(
17
18
  version?: string,
18
19
  serviceProviders?: ReadonlyArray<ServiceProvider>,
19
20
  options?: BaseCLIFeatureOptions,
21
+ startupTasks?: ReadonlyArray<StartupTask>,
20
22
  ): Promise<RunResult> {
21
23
  if (!name) {
22
24
  name = path.basename(process.execPath);
@@ -37,7 +39,8 @@ export async function launchSingleCommandCLI(
37
39
 
38
40
  cli.addCommand(command);
39
41
 
40
- serviceProviders?.forEach((service) => cli.addServiceProvider(service));
42
+ serviceProviders?.forEach((serviceProvider) => cli.addServiceProvider(serviceProvider));
43
+ startupTasks?.forEach((startupTask) => cli.addStartupTask(startupTask));
41
44
 
42
45
  return await cli.run();
43
46
  }
@@ -49,6 +52,7 @@ export async function launchMultiCommandCLI(
49
52
  version?: string,
50
53
  serviceProviders?: ReadonlyArray<ServiceProvider>,
51
54
  options?: BaseCLIFeatureOptions,
55
+ startupTasks?: ReadonlyArray<StartupTask>,
52
56
  ): Promise<RunResult> {
53
57
  if (!name) {
54
58
  name = path.basename(process.execPath);
@@ -69,7 +73,8 @@ export async function launchMultiCommandCLI(
69
73
 
70
74
  commands.forEach((command) => cli.addCommand(command));
71
75
 
72
- serviceProviders?.forEach((service) => cli.addServiceProvider(service));
76
+ serviceProviders?.forEach((serviceProvider) => cli.addServiceProvider(serviceProvider));
77
+ startupTasks?.forEach((startupTask) => cli.addStartupTask(startupTask));
73
78
 
74
79
  return await cli.run();
75
80
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Common shape shared by {@link StartupTask} and {@link ShutdownTask}: a task with an optional
3
+ * priority, used to order execution. Higher values run earlier. Undefined is treated as `0`.
4
+ */
5
+ export interface PriorityTask {
6
+ readonly priority?: number;
7
+ }
8
+
9
+ /**
10
+ * Stably sort tasks by descending priority (default `0`). Tasks with the same priority keep
11
+ * their relative order in the input array.
12
+ */
13
+ export function sortByPriority<T extends PriorityTask>(tasks: ReadonlyArray<T>): Array<T> {
14
+ return tasks
15
+ .map((task, index) => ({ task, index }))
16
+ .sort((a, b) => (b.task.priority ?? 0) - (a.task.priority ?? 0) || a.index - b.index)
17
+ .map((entry) => entry.task);
18
+ }
19
+
20
+ /**
21
+ * Internal helper shared by {@link DefaultStartupService} and {@link DefaultShutdownService}:
22
+ * accumulates registered tasks and returns them in priority order on demand.
23
+ */
24
+ export default class PriorityTaskList<T extends PriorityTask> {
25
+ readonly #tasks: Array<T> = [];
26
+
27
+ public add(task: T): void {
28
+ this.#tasks.push(task);
29
+ }
30
+
31
+ public sorted(): ReadonlyArray<T> {
32
+ return sortByPriority(this.#tasks);
33
+ }
34
+ }
@@ -21,6 +21,7 @@ import { scanForGlobalModifierCommandClauses, scanForNonModifierCommandClause }
21
21
  import type { ServiceProviderRegistry } from "@flowscripter/dynamic-cli-framework-api";
22
22
  import { parseGlobalCommandClause, type ParseResult, parseSubCommandClause } from "./parser.ts";
23
23
  import type ConfigurationServiceProvider from "../service/configuration/ConfigurationServiceProvider.ts";
24
+ import type KeyValueServiceProvider from "../service/configuration/KeyValueServiceProvider.ts";
24
25
  import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
25
26
  import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
26
27
  import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
@@ -28,9 +29,35 @@ import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
28
29
  import type { Command } from "@flowscripter/dynamic-cli-framework-api";
29
30
  import type { ArgumentPrompterService } from "@flowscripter/dynamic-cli-framework-api";
30
31
  import { ARGUMENT_PROMPTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
32
+ import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
33
+ import type { ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
34
+ import DefaultStartupService from "../service/startup/DefaultStartupService.ts";
35
+ import { sortByPriority } from "./lifecycle/PriorityTaskList.ts";
31
36
 
32
37
  const logger = getLogger("runner");
33
38
 
39
+ /**
40
+ * Wrap a {@link ServiceProvider}'s `initService()` call as a {@link StartupTask} so it can be
41
+ * merged with directly-registered {@link StartupTask}s (e.g. the banner task) into a single
42
+ * priority-ordered sequence.
43
+ */
44
+ function toProviderStartupTask(
45
+ serviceProvider: ServiceProvider,
46
+ commandRegistry: CommandRegistry,
47
+ ): StartupTask {
48
+ return {
49
+ id: serviceProvider.serviceId,
50
+ priority: serviceProvider.servicePriority,
51
+ mode: "blocking",
52
+ modifierCommands: Array.from(
53
+ commandRegistry
54
+ .getGlobalModifierCommandsByNameProvidedByService(serviceProvider.serviceId)
55
+ .values(),
56
+ ),
57
+ run: (context: Context) => serviceProvider.initService(context),
58
+ };
59
+ }
60
+
34
61
  async function attemptPromptForMissingArguments(
35
62
  parseResult: ParseResult,
36
63
  context: Context,
@@ -58,31 +85,26 @@ async function attemptPromptForMissingArguments(
58
85
  *
59
86
  * @param parseResult the {@link ParseResult} to execute.
60
87
  * @param context the {@link Context} to use.
61
- * @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
88
+ * @param keyValueServiceProvider optional {@link KeyValueServiceProvider} to use to get a scope-decorated {@link Context}.
62
89
  * @param isDefaultCommand whether the command is the default command for the CLI.
63
90
  */
64
91
  async function executeParsedCommand(
65
92
  parseResult: ParseResult,
66
93
  context: Context,
67
- configurationServiceProvider: ConfigurationServiceProvider | undefined,
94
+ keyValueServiceProvider: KeyValueServiceProvider | undefined,
68
95
  isDefaultCommand = false,
69
96
  ): Promise<RunResult> {
70
97
  try {
71
98
  if (parseResult.groupCommand !== undefined) {
72
99
  logger.debug("Executing group command with name: %s", parseResult!.groupCommand!.name);
73
- if (
74
- configurationServiceProvider?.keyValueServiceEnabled ||
75
- configurationServiceProvider?.secretServiceEnabled
76
- ) {
77
- configurationServiceProvider.setCommandKeyValueScope(parseResult!.groupCommand!.name);
78
- }
79
- await parseResult.groupCommand.execute(context);
80
- if (
81
- configurationServiceProvider?.keyValueServiceEnabled ||
82
- configurationServiceProvider?.secretServiceEnabled
83
- ) {
84
- await configurationServiceProvider.clearKeyValueScope();
85
- }
100
+ const groupContext = keyValueServiceProvider
101
+ ? keyValueServiceProvider.getContextForScope(
102
+ context,
103
+ "command",
104
+ parseResult!.groupCommand!.name,
105
+ )
106
+ : context;
107
+ await parseResult.groupCommand.execute(groupContext);
86
108
  }
87
109
 
88
110
  logger.debug(
@@ -91,26 +113,20 @@ async function executeParsedCommand(
91
113
  parseResult!.populatedArgumentValues,
92
114
  );
93
115
 
94
- if (
95
- configurationServiceProvider?.keyValueServiceEnabled ||
96
- configurationServiceProvider?.secretServiceEnabled
97
- ) {
98
- configurationServiceProvider.setCommandKeyValueScope(parseResult.command.name);
99
- }
116
+ const commandContext = keyValueServiceProvider
117
+ ? keyValueServiceProvider.getContextForScope(context, "command", parseResult.command.name)
118
+ : context;
100
119
  if (isSubCommand(parseResult.command)) {
101
- await parseResult.command.execute(context, parseResult.populatedArgumentValues as Values);
120
+ await parseResult.command.execute(
121
+ commandContext,
122
+ parseResult.populatedArgumentValues as Values,
123
+ );
102
124
  } else {
103
125
  await (parseResult.command as GlobalCommand).execute(
104
- context,
126
+ commandContext,
105
127
  parseResult.populatedArgumentValues as SingleValueType,
106
128
  );
107
129
  }
108
- if (
109
- configurationServiceProvider?.keyValueServiceEnabled ||
110
- configurationServiceProvider?.secretServiceEnabled
111
- ) {
112
- await configurationServiceProvider.clearKeyValueScope();
113
- }
114
130
  } catch (err) {
115
131
  if ((err as Error).message === "Interrupted") {
116
132
  throw err;
@@ -136,6 +152,7 @@ async function executeParsedCommand(
136
152
  * @param globalModifierCommandsByName a map of non-modifier {@link Command} instances by name to parse for.
137
153
  * @param globalModifierCommandsByShortAlias a map of {@link GlobalModifierCommand} instances by short alias to use when scanning.
138
154
  * @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
155
+ * @param keyValueServiceProvider optional {@link KeyValueServiceProvider} to use to get a scope-decorated {@link Context}.
139
156
  * @param context the {@link Context} to use.
140
157
  */
141
158
  async function findAndExecuteGlobalModifierCommands(
@@ -144,6 +161,7 @@ async function findAndExecuteGlobalModifierCommands(
144
161
  globalModifierCommandsByName: ReadonlyMap<string, GlobalModifierCommand>,
145
162
  globalModifierCommandsByShortAlias: ReadonlyMap<string, GlobalModifierCommand>,
146
163
  configurationServiceProvider: ConfigurationServiceProvider | undefined,
164
+ keyValueServiceProvider: KeyValueServiceProvider | undefined,
147
165
  context: Context,
148
166
  ): Promise<RunResult | undefined> {
149
167
  // build a list of GlobalModifierCommands to execute
@@ -241,11 +259,7 @@ async function findAndExecuteGlobalModifierCommands(
241
259
  );
242
260
  for (const parseResult of globalModifierCommandParseResults) {
243
261
  // execute and fast fail on error
244
- const runResult = await executeParsedCommand(
245
- parseResult,
246
- context,
247
- configurationServiceProvider,
248
- );
262
+ const runResult = await executeParsedCommand(parseResult, context, keyValueServiceProvider);
249
263
  if (runResult.runState !== RunState.SUCCESS) {
250
264
  return runResult;
251
265
  }
@@ -261,6 +275,7 @@ async function findAndExecuteGlobalModifierCommands(
261
275
  * @param globalCommandsByShortAlias the map of {@link GlobalCommand} instances by short alias to use when scanning.
262
276
  * @param groupAndMemberCommandsByJoinedName optional map of {@link GroupCommand} and member {@link SubCommand} instances to use when scanning.
263
277
  * @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
278
+ * @param keyValueServiceProvider optional {@link KeyValueServiceProvider} to use to get a scope-decorated {@link Context}.
264
279
  * @param context the {@link Context} to use.
265
280
  */
266
281
  async function findAndExecuteNonModifierCommand(
@@ -272,6 +287,7 @@ async function findAndExecuteNonModifierCommand(
272
287
  | ReadonlyMap<string, { groupCommand: GroupCommand; command: SubCommand }>
273
288
  | undefined,
274
289
  configurationServiceProvider: ConfigurationServiceProvider | undefined,
290
+ keyValueServiceProvider: KeyValueServiceProvider | undefined,
275
291
  context: Context,
276
292
  ): Promise<RunResult | undefined> {
277
293
  let parseResult: ParseResult | undefined;
@@ -383,7 +399,7 @@ async function findAndExecuteNonModifierCommand(
383
399
  await printUnusedArgsWarning(context, unusedArgs.flat());
384
400
  }
385
401
 
386
- return executeParsedCommand(parseResult, context, configurationServiceProvider);
402
+ return executeParsedCommand(parseResult, context, keyValueServiceProvider);
387
403
  }
388
404
 
389
405
  /**
@@ -393,6 +409,7 @@ async function findAndExecuteNonModifierCommand(
393
409
  * @param unusedArgs an array to which any unused argument sequences will be added.
394
410
  * @param defaultNonModifierCommand the default non-modifier {@link Command} to use.
395
411
  * @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use to get default argument values.
412
+ * @param keyValueServiceProvider optional {@link KeyValueServiceProvider} to use to get a scope-decorated {@link Context}.
396
413
  * @param context the {@link Context} to use.
397
414
  */
398
415
  async function findAndExecuteDefaultNonModifierCommand(
@@ -400,6 +417,7 @@ async function findAndExecuteDefaultNonModifierCommand(
400
417
  unusedArgs: Array<ReadonlyArray<string>>,
401
418
  defaultNonModifierCommand: Command,
402
419
  configurationServiceProvider: ConfigurationServiceProvider | undefined,
420
+ keyValueServiceProvider: KeyValueServiceProvider | undefined,
403
421
  context: Context,
404
422
  ): Promise<RunResult | undefined> {
405
423
  const defaultArgumentValues = configurationServiceProvider
@@ -511,7 +529,7 @@ async function findAndExecuteDefaultNonModifierCommand(
511
529
  await printUnusedArgsWarning(context, unusedArgs.flat());
512
530
  }
513
531
 
514
- return executeParsedCommand(parseResult, context, configurationServiceProvider, true);
532
+ return executeParsedCommand(parseResult, context, keyValueServiceProvider, true);
515
533
  }
516
534
 
517
535
  /**
@@ -520,14 +538,15 @@ async function findAndExecuteDefaultNonModifierCommand(
520
538
  *
521
539
  * An overview of the logic (assuming the optional ConfigurationServiceProvider is provided):
522
540
  *
523
- * 1. For each ServiceProvider in servicePriority order:
524
- * - Scan arguments for GlobalModifierCommand clauses provided by the ServiceProvider
541
+ * 1. For each StartupTask in priority order (whether backed by a ServiceProvider's
542
+ * initService() or registered directly, e.g. the banner task):
543
+ * - Scan arguments for GlobalModifierCommand clauses provided by the task
525
544
  * - For each discovered clause:
526
545
  * - Set any argument defaults from ConfigurationServiceProvider
527
546
  * - Parse the arguments for the GlobalModifierCommand
528
547
  * - Return on error
529
548
  * - Add to list of GlobalModifierCommands to execute
530
- * - Scan ConfigurationServiceProvider for GlobalModifierCommand clauses provided by the ServiceProvider not already found in arguments
549
+ * - Scan ConfigurationServiceProvider for GlobalModifierCommand clauses provided by the task not already found in arguments
531
550
  * - For each configured clause:
532
551
  * - Parse the arguments for the GlobalModifierCommand
533
552
  * - Return on error
@@ -536,14 +555,18 @@ async function findAndExecuteDefaultNonModifierCommand(
536
555
  * - For each GlobalModifierCommand clause:
537
556
  * - Execute the GlobalModifierCommand
538
557
  * - Return on error
539
- * - Init the ServiceProvider's service.
540
- * 2. Scan arguments for any GlobalModifierCommand clauses not provided by ServiceProviders
558
+ * - Give the task a Context whose KeyValueService is bound permanently to its own scope
559
+ * (keyed by task.id), via KeyValueServiceProvider if provided.
560
+ * - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
561
+ * every ServiceProvider's initService() behaves); fired without awaiting if "background"
562
+ * (errors logged, not propagated).
563
+ * 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
541
564
  * - For each discovered clause:
542
565
  * - Set any argument defaults from ConfigurationServiceProvider
543
566
  * - Parse the arguments for the GlobalModifierCommand
544
567
  * - Return on error
545
568
  * - Add to list of GlobalModifierCommands to execute
546
- * 3. Scan ConfigurationServiceProvider for GlobalModifierCommand clauses not provided by the ServiceProvider and not already found in arguments
569
+ * 3. Scan ConfigurationServiceProvider for GlobalModifierCommand clauses not provided by a StartupTask and not already found in arguments
547
570
  * - For each discovered clause:
548
571
  * - Parse the arguments for the GlobalModifierCommand
549
572
  * - Return on error
@@ -578,6 +601,7 @@ async function findAndExecuteDefaultNonModifierCommand(
578
601
  * @param commandRegistry the {@link CommandRegistry} to use when scanning and parsing.
579
602
  * @param serviceProviderRegistry the {@link ServiceProviderRegistry} to use when scanning and parsing.
580
603
  * @param configurationServiceProvider optional {@link ConfigurationServiceProvider} to use for accessing argument defaults.
604
+ * @param keyValueServiceProvider optional {@link KeyValueServiceProvider} to use to get a scope-decorated {@link Context}.
581
605
  * @param context the {@link Context} in which to execute specified {@link Command} instances.
582
606
  * @param defaultCommand optional {@link SubCommand} or {@link GlobalCommand} implementation to attempt to parse arguments for and execute if
583
607
  * no non-modifier {@link Command} name is identified in the provided arguments.
@@ -587,8 +611,10 @@ export async function run(
587
611
  commandRegistry: CommandRegistry,
588
612
  serviceProviderRegistry: ServiceProviderRegistry,
589
613
  configurationServiceProvider: ConfigurationServiceProvider | undefined,
614
+ keyValueServiceProvider: KeyValueServiceProvider | undefined,
590
615
  context: Context,
591
616
  defaultCommand?: Command,
617
+ startupService?: DefaultStartupService,
592
618
  ): Promise<RunResult> {
593
619
  if (defaultCommand && !isSubCommand(defaultCommand) && !isGlobalCommand(defaultCommand)) {
594
620
  throw new Error(
@@ -601,17 +627,29 @@ export async function run(
601
627
  let availableArgs: ReadonlyArray<ReadonlyArray<string>> = [args];
602
628
  let unusedArgs: Array<ReadonlyArray<string>> = [];
603
629
 
604
- // for each ServiceProvider (they are returned in servicePriority order)
605
- for (const serviceProvider of serviceProviderRegistry.getServiceProviders()) {
606
- // using the GlobalModifierCommands instances provided by the ServiceProvider...
607
- const globalModifierCommandsByName =
608
- commandRegistry.getGlobalModifierCommandsByNameProvidedByService(serviceProvider.serviceId);
609
- const globalModifierCommandsByShortAlias =
610
- commandRegistry.getGlobalModifierCommandsByShortAliasProvidedByService(
611
- serviceProvider.serviceId,
612
- );
630
+ // merge every ServiceProvider's initService() into a StartupTask, then combine with any
631
+ // directly-registered StartupTasks (e.g. the banner task) into one priority-ordered sequence
632
+ const startupTasks = sortByPriority<StartupTask>([
633
+ ...serviceProviderRegistry
634
+ .getServiceProviders()
635
+ .map((serviceProvider) => toProviderStartupTask(serviceProvider, commandRegistry)),
636
+ ...(startupService ? startupService.getTasks() : []),
637
+ ]);
638
+
639
+ for (const task of startupTasks) {
640
+ // using the GlobalModifierCommands instances provided by the task...
641
+ const globalModifierCommandsByName = new Map(
642
+ (task.modifierCommands ?? []).map((command) => [command.name, command]),
643
+ );
644
+ const globalModifierCommandsByShortAlias = new Map(
645
+ (task.modifierCommands ?? [])
646
+ .filter((command): command is GlobalModifierCommand & { shortAlias: string } =>
647
+ Boolean(command.shortAlias),
648
+ )
649
+ .map((command) => [command.shortAlias, command]),
650
+ );
613
651
 
614
- // if there are GlobalModifierCommands provided by the ServiceProvider...
652
+ // if there are GlobalModifierCommands provided by the task...
615
653
  if (globalModifierCommandsByName.size > 0) {
616
654
  // ...scan arguments, parse any resulting clauses and execute any resulting parsed GlobalModifierCommands
617
655
  const runResult = await findAndExecuteGlobalModifierCommands(
@@ -620,6 +658,7 @@ export async function run(
620
658
  globalModifierCommandsByName,
621
659
  globalModifierCommandsByShortAlias,
622
660
  configurationServiceProvider,
661
+ keyValueServiceProvider,
623
662
  context,
624
663
  );
625
664
 
@@ -633,23 +672,20 @@ export async function run(
633
672
  unusedArgs = [];
634
673
  }
635
674
 
636
- // init the service provider's service
675
+ logger.debug("Running startup task with ID: %s", task.id);
637
676
 
638
- logger.debug("Initialising service with ID: %s", serviceProvider.serviceId);
677
+ // each task gets a Context whose KeyValueService is bound permanently to its own scope (keyed
678
+ // by task.id)
679
+ const taskContext = keyValueServiceProvider
680
+ ? keyValueServiceProvider.getContextForScope(context, "service", task.id)
681
+ : context;
639
682
 
640
- if (
641
- configurationServiceProvider?.keyValueServiceEnabled ||
642
- configurationServiceProvider?.secretServiceEnabled
643
- ) {
644
- configurationServiceProvider.setServiceKeyValueScope(serviceProvider.serviceId);
645
- }
646
-
647
- await serviceProvider.initService(context);
648
- if (
649
- configurationServiceProvider?.keyValueServiceEnabled ||
650
- configurationServiceProvider?.secretServiceEnabled
651
- ) {
652
- await configurationServiceProvider.clearKeyValueScope();
683
+ if ((task.mode ?? "blocking") === "blocking") {
684
+ await task.run(taskContext);
685
+ } else {
686
+ void task.run(taskContext).catch((error: unknown) => {
687
+ logger.debug(() => `Background startup task '${task.id}' failed: ${error}`);
688
+ });
653
689
  }
654
690
  }
655
691
 
@@ -668,6 +704,7 @@ export async function run(
668
704
  globalModifierCommandsByName,
669
705
  globalModifierCommandsByShortAlias,
670
706
  configurationServiceProvider,
707
+ keyValueServiceProvider,
671
708
  context,
672
709
  );
673
710
 
@@ -695,6 +732,7 @@ export async function run(
695
732
  globalCommandsByShortAlias,
696
733
  groupAndMemberCommandByJoinedName,
697
734
  configurationServiceProvider,
735
+ keyValueServiceProvider,
698
736
  context,
699
737
  );
700
738
 
@@ -715,6 +753,7 @@ export async function run(
715
753
  unusedArgs,
716
754
  defaultCommand,
717
755
  configurationServiceProvider,
756
+ keyValueServiceProvider,
718
757
  context,
719
758
  );
720
759