@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
@@ -0,0 +1,246 @@
1
+ import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
+ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
+ import type {
5
+ KeyValueService,
6
+ ShutdownService,
7
+ ValueNode,
8
+ } from "@flowscripter/dynamic-cli-framework-api";
9
+ import { KEY_VALUE_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
10
+ import DefaultKeyValueService from "./DefaultKeyValueService.ts";
11
+ import DefaultSecretService from "./DefaultSecretService.ts";
12
+ import type ConfigurationServiceProvider from "./ConfigurationServiceProvider.ts";
13
+ import type { KeyValueServiceScopeType } from "./ConfigurationServiceProvider.ts";
14
+
15
+ /**
16
+ * Registered under {@link KEY_VALUE_SERVICE_ID} in the plain, undecorated {@link Context} purely
17
+ * so {@link Context.doesServiceExist} reports correctly - every real access goes through a
18
+ * per-scope instance from {@link KeyValueServiceProvider.getScopedKeyValueService}, applied
19
+ * via a per-call decorated {@link Context} (see `runner.ts`). Reaching any of these methods
20
+ * directly means that decoration was somehow bypassed.
21
+ */
22
+ class UnreachableKeyValueService implements KeyValueService {
23
+ get<T extends ValueNode>(): Promise<T> {
24
+ return Promise.reject(new Error(UnreachableKeyValueService.#message));
25
+ }
26
+
27
+ has(): Promise<boolean> {
28
+ return Promise.reject(new Error(UnreachableKeyValueService.#message));
29
+ }
30
+
31
+ set(): Promise<void> {
32
+ return Promise.reject(new Error(UnreachableKeyValueService.#message));
33
+ }
34
+
35
+ delete(): Promise<void> {
36
+ return Promise.reject(new Error(UnreachableKeyValueService.#message));
37
+ }
38
+
39
+ static readonly #message =
40
+ "KeyValueService accessed without a resolved scope - this should be unreachable";
41
+ }
42
+
43
+ /**
44
+ * Provides a generic, per-scope key-value store for commands and services ({@link KeyValueService}).
45
+ *
46
+ * The underlying data is read from/written to the same configuration file managed by
47
+ * {@link ConfigurationServiceProvider} - see {@link ConfigurationServiceProvider.getKeyValueData} and
48
+ * {@link ConfigurationServiceProvider.flushIfDirty}, reached via a direct constructor reference wired
49
+ * up by `BaseCLI` (never via {@link Context}, since that would expose config-file read/write access
50
+ * more broadly than intended).
51
+ *
52
+ * Its `serviceId` is {@link KEY_VALUE_SERVICE_ID} and its {@link ServiceInfo.service} is a
53
+ * placeholder ({@link UnreachableKeyValueService}) - real access always goes through a per-consumer
54
+ * decorated {@link Context} from {@link getContextForScope}, applied by `runner.ts`.
55
+ *
56
+ * Any node within a value passed to {@link KeyValueService.set} can be wrapped in {@link Secret} -
57
+ * at any depth - to have that node, and only that node, stored as an OS-native secret. When a node
58
+ * is wrapped in {@link Secret}, its value (of any shape) is JSON-serialized and stored as a single
59
+ * OS-native secret via Bun.secrets, with a sentinel value of the format `__SECRET__:<bun_secret_name>`
60
+ * substituted in its place in the structure that gets stored in the config file. Everything else in
61
+ * the value is stored as plain (unencrypted) config data. The sentinel prefix `__SECRET__:` is
62
+ * reserved and must not be used for regular key-value data.
63
+ *
64
+ * Independently of how a value was written, {@link KeyValueService.get} recursively resolves any
65
+ * string leaf - at any depth within the retrieved value - which starts with the sentinel prefix,
66
+ * via the OS secret store. This means a secret reference may also be hand-embedded (nested
67
+ * arbitrarily deep) directly within a plain, non-secret value in the config file.
68
+ *
69
+ * Secret support requires `secretServiceEnabled=true` in the constructor (which also requires
70
+ * `keyValueServiceEnabled=true` on the {@link ConfigurationServiceProvider} to have config enabled).
71
+ *
72
+ * As an example of the underlying config file structure:
73
+ * ```
74
+ * {
75
+ * "defaults": {
76
+ * ...
77
+ * },
78
+ * "key-values": {
79
+ * "commands": {
80
+ * "command1": {
81
+ * "foo1": "bar1",
82
+ * "foo2": "__SECRET__:command_command1_foo2",
83
+ * "foo3": {
84
+ * "nested": ["a", "__SECRET__:command_command1_foo3_nested"]
85
+ * }
86
+ * },
87
+ * "command2": {
88
+ * "foo1": "bar3"
89
+ * }
90
+ * },
91
+ * "services": {
92
+ * "service-id-1": {
93
+ * "foo1": "bar"
94
+ * }
95
+ * }
96
+ * }
97
+ * }
98
+ * ```
99
+ */
100
+ export default class KeyValueServiceProvider implements ServiceProvider {
101
+ readonly serviceId: string = KEY_VALUE_SERVICE_ID;
102
+ readonly servicePriority: number;
103
+
104
+ readonly #configurationServiceProvider: ConfigurationServiceProvider;
105
+
106
+ public readonly keyValueServiceEnabled: boolean;
107
+ public readonly secretServiceEnabled: boolean;
108
+
109
+ // per-scope KeyValueService instances, created on first request and cached thereafter - each is
110
+ // bound permanently to one scope's Map (and one scope-prefixed DefaultSecretService), never
111
+ // re-pointed. See getScopedKeyValueService().
112
+ readonly #commandScopedKeyValueServices = new Map<string, DefaultKeyValueService>();
113
+ readonly #serviceScopedKeyValueServices = new Map<string, DefaultKeyValueService>();
114
+
115
+ // used to construct scoped DefaultSecretService instances on demand.
116
+ #cliConfigName: string | undefined;
117
+
118
+ /**
119
+ * Create an instance of the service provider with the specified details.
120
+ *
121
+ * @param servicePriority the priority of the service.
122
+ * @param configurationServiceProvider the {@link ConfigurationServiceProvider} whose config file
123
+ * backs this provider's data - an internal wiring reference, never exposed further.
124
+ * @param keyValueServiceEnabled optionally provide a {@link KeyValueService} implementation: `configurationServiceProvider.configEnabled` must be true in this case
125
+ * @param secretServiceEnabled optionally enable OS-native secret storage via Bun.secrets: `configurationServiceProvider.configEnabled` must be true in this case
126
+ */
127
+ public constructor(
128
+ servicePriority: number,
129
+ configurationServiceProvider: ConfigurationServiceProvider,
130
+ keyValueServiceEnabled = false,
131
+ secretServiceEnabled = false,
132
+ ) {
133
+ if (!configurationServiceProvider.configEnabled && keyValueServiceEnabled) {
134
+ throw new Error("configEnabled must be true if keyValueServiceEnabled is true");
135
+ }
136
+ if (!configurationServiceProvider.configEnabled && secretServiceEnabled) {
137
+ throw new Error("configEnabled must be true if secretServiceEnabled is true");
138
+ }
139
+ this.servicePriority = servicePriority;
140
+ this.#configurationServiceProvider = configurationServiceProvider;
141
+ this.keyValueServiceEnabled = keyValueServiceEnabled;
142
+ this.secretServiceEnabled = secretServiceEnabled;
143
+ }
144
+
145
+ public getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo> {
146
+ this.#cliConfigName = cliConfig.name;
147
+ return Promise.resolve({
148
+ // this is a placeholder - see UnreachableKeyValueService - unless neither is enabled
149
+ service:
150
+ this.keyValueServiceEnabled || this.secretServiceEnabled
151
+ ? new UnreachableKeyValueService()
152
+ : undefined,
153
+ commands: [],
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Return the {@link KeyValueService} bound to the given scope, creating (and caching) it on
159
+ * first request. The returned instance is permanently bound to that scope's data and secret
160
+ * prefix - it is never re-pointed, so it may be held and used for as long as its owning
161
+ * command/service/task is alive without racing any other scope's window.
162
+ *
163
+ * @param scopeType whether `scopeKey` is a command name or a service/task ID.
164
+ * @param scopeKey the command name or service/task ID to scope the key-value data to.
165
+ *
166
+ * @throws {Error} if neither `keyValueServiceEnabled` nor `secretServiceEnabled` is set.
167
+ */
168
+ public getScopedKeyValueService(
169
+ scopeType: KeyValueServiceScopeType,
170
+ scopeKey: string,
171
+ ): KeyValueService {
172
+ if (!this.keyValueServiceEnabled && !this.secretServiceEnabled) {
173
+ throw new Error(`Attempt to use KeyValueService/SecretService which is not enabled`);
174
+ }
175
+
176
+ const scopedServices =
177
+ scopeType === "command"
178
+ ? this.#commandScopedKeyValueServices
179
+ : this.#serviceScopedKeyValueServices;
180
+ const cached = scopedServices.get(scopeKey);
181
+ if (cached) {
182
+ return cached;
183
+ }
184
+
185
+ const keyValueData = this.#configurationServiceProvider.getKeyValueData(scopeType, scopeKey);
186
+
187
+ const secretService = this.secretServiceEnabled
188
+ ? new DefaultSecretService(this.#cliConfigName!, `${scopeType}_${scopeKey}`)
189
+ : undefined;
190
+ const scopedService = new DefaultKeyValueService(keyValueData, secretService);
191
+ scopedServices.set(scopeKey, scopedService);
192
+ return scopedService;
193
+ }
194
+
195
+ /**
196
+ * Return a {@link Context} which delegates every lookup to `context` unchanged, except
197
+ * `getServiceById(KEY_VALUE_SERVICE_ID)`, which resolves to the scope-bound instance from
198
+ * {@link getScopedKeyValueService} for the given scope. Used by `runner.ts` to give each
199
+ * command/service/task a KeyValueService isolated to its own scope, invisibly at the call site.
200
+ *
201
+ * Returns `context` itself, unchanged, if neither `keyValueServiceEnabled` nor
202
+ * `secretServiceEnabled` is set.
203
+ */
204
+ public getContextForScope(
205
+ context: Context,
206
+ scopeType: KeyValueServiceScopeType,
207
+ scopeKey: string,
208
+ ): Context {
209
+ if (!this.keyValueServiceEnabled && !this.secretServiceEnabled) {
210
+ return context;
211
+ }
212
+ const scopedKeyValueService = this.getScopedKeyValueService(scopeType, scopeKey);
213
+ return {
214
+ cliConfig: context.cliConfig,
215
+ getServiceById: (id: string): unknown =>
216
+ id === KEY_VALUE_SERVICE_ID ? scopedKeyValueService : context.getServiceById(id),
217
+ doesServiceExist: (id: string): boolean => context.doesServiceExist(id),
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Flush every scoped KeyValueService's data to the configuration file, if any of them report
223
+ * themselves dirty. Registered as a low-priority ShutdownTask (see {@link initService}) so it
224
+ * runs once, after other shutdown cleanup, regardless of how long any scope was written to.
225
+ */
226
+ async #flushDirtyScopes(): Promise<void> {
227
+ const anyDirty = [
228
+ ...this.#commandScopedKeyValueServices.values(),
229
+ ...this.#serviceScopedKeyValueServices.values(),
230
+ ].some((service) => service.isDirty());
231
+ await this.#configurationServiceProvider.flushIfDirty(anyDirty);
232
+ }
233
+
234
+ public async initService(context: Context): Promise<void> {
235
+ if (this.keyValueServiceEnabled || this.secretServiceEnabled) {
236
+ const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
237
+ shutdownService.registerTask({
238
+ id: `${KEY_VALUE_SERVICE_ID}-flush`,
239
+ // low priority - shutdown tasks run in descending priority order, so this runs after
240
+ // other shutdown cleanup (which defaults to priority 0).
241
+ priority: -100,
242
+ run: () => this.#flushDirtyScopes(),
243
+ });
244
+ }
245
+ }
246
+ }
@@ -2,10 +2,14 @@ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-
2
2
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type ConfigurationServiceProvider from "../ConfigurationServiceProvider.ts";
5
+ import {
6
+ CONFIGURATION_SERVICE_ID,
7
+ type ConfigurationService,
8
+ } from "@flowscripter/dynamic-cli-framework-api";
6
9
 
7
10
  /**
8
- * Command allowing the specification of the configuration file location used by the {@link ConfigurationServiceProvider}.
11
+ * Command allowing the specification of the configuration file location used by
12
+ * {@link ConfigurationServiceProvider}.
9
13
  */
10
14
  export default class ConfigCommand implements GlobalModifierCommand {
11
15
  readonly name = "config";
@@ -17,20 +21,17 @@ export default class ConfigCommand implements GlobalModifierCommand {
17
21
  };
18
22
  readonly executePriority: number;
19
23
 
20
- readonly #configurationServiceProvider: ConfigurationServiceProvider;
21
-
22
- public constructor(
23
- configurationServiceProvider: ConfigurationServiceProvider,
24
- executePriority: number,
25
- ) {
26
- this.#configurationServiceProvider = configurationServiceProvider;
24
+ public constructor(executePriority: number) {
27
25
  this.executePriority = executePriority;
28
26
  }
29
27
 
30
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
28
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
31
29
  const configLocation = argumentValue as string;
32
30
 
33
- this.#configurationServiceProvider.setConfigLocation(configLocation);
31
+ const configurationService = context.getServiceById(
32
+ CONFIGURATION_SERVICE_ID,
33
+ ) as ConfigurationService;
34
+ configurationService.setConfigLocation(configLocation);
34
35
 
35
36
  return Promise.resolve();
36
37
  }
@@ -1,22 +1,26 @@
1
1
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
3
- import type ConfigurationServiceProvider from "../ConfigurationServiceProvider.ts";
4
3
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
5
4
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
5
  import type { SyntaxHighlighterService } from "@flowscripter/dynamic-cli-framework-api";
7
6
  import { SYNTAX_HIGHLIGHTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
8
7
 
9
8
  /**
10
- * Command which dumps the configuration loaded by the {@link ConfigurationServiceProvider}.
9
+ * Command which dumps the configuration loaded by {@link ConfigurationServiceProvider}.
10
+ *
11
+ * Takes a direct reference to {@link ConfigurationServiceProvider.getConfigString}, wired by the
12
+ * provider that constructs this command, rather than looking it up via {@link Context} -
13
+ * deliberately, since a full config dump is not something every command/task should be able to
14
+ * reach through a general lookup (see {@link ConfigurationService}'s doc comment).
11
15
  */
12
16
  export default class DumpConfigCommand implements GlobalCommand {
13
17
  readonly name = "dump-config";
14
18
  readonly description = "Dump configuration values";
15
19
 
16
- readonly #configurationServiceProvider: ConfigurationServiceProvider;
20
+ readonly #getConfigString: () => string;
17
21
 
18
- public constructor(configurationServiceProvider: ConfigurationServiceProvider) {
19
- this.#configurationServiceProvider = configurationServiceProvider;
22
+ public constructor(getConfigString: () => string) {
23
+ this.#getConfigString = getConfigString;
20
24
  }
21
25
 
22
26
  public async execute(context: Context): Promise<void> {
@@ -26,10 +30,7 @@ export default class DumpConfigCommand implements GlobalCommand {
26
30
  ) as SyntaxHighlighterService;
27
31
 
28
32
  await printerService.print(
29
- `${syntaxHighlighterService.highlight(
30
- this.#configurationServiceProvider.getConfigString(),
31
- "json",
32
- )}\n`,
33
+ `${syntaxHighlighterService.highlight(this.#getConfigString(), "json")}\n`,
33
34
  );
34
35
  }
35
36
  }
@@ -1,24 +1,26 @@
1
1
  import type {
2
+ Context,
2
3
  FetchOptions,
3
4
  FetchService,
4
5
  ShutdownService,
5
6
  } from "@flowscripter/dynamic-cli-framework-api";
7
+ import { SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
8
  import getLogger from "../../util/logger.ts";
7
9
 
8
10
  const logger = getLogger("DefaultFetchService");
9
11
 
10
12
  export default class DefaultFetchService implements FetchService {
11
- #shutdownService: ShutdownService | undefined;
13
+ #context: Context | undefined;
12
14
 
13
- public setDependencies(shutdownService: ShutdownService): void {
14
- this.#shutdownService = shutdownService;
15
+ public setContext(context: Context): void {
16
+ this.#context = context;
15
17
  }
16
18
 
17
19
  public async fetch(input: string | URL, options: FetchOptions = {}): Promise<Response> {
18
- if (this.#shutdownService === undefined) {
19
- throw new Error("DefaultFetchService.fetch() called before setDependencies()");
20
+ if (this.#context === undefined) {
21
+ throw new Error("DefaultFetchService.fetch() called before setContext()");
20
22
  }
21
- const shutdownService = this.#shutdownService;
23
+ const shutdownService = this.#context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
22
24
  const { timeoutMs, longRunning = false, signal: callerSignal, ...requestInit } = options;
23
25
 
24
26
  const controller = new AbortController();
@@ -34,12 +36,16 @@ export default class DefaultFetchService implements FetchService {
34
36
  let settled = false;
35
37
  if (longRunning) {
36
38
  shutdownService.enterLongRunningMode();
37
- shutdownService.addShutdownListener(async () => {
38
- if (settled) {
39
- return;
40
- }
41
- logger.debug(() => `Aborting fetch of '${input.toString()}' due to shutdown`);
42
- controller.abort();
39
+ shutdownService.registerTask({
40
+ id: `fetch:${input.toString()}`,
41
+ priority: 0,
42
+ run: async () => {
43
+ if (settled) {
44
+ return;
45
+ }
46
+ logger.debug(() => `Aborting fetch of '${input.toString()}' due to shutdown`);
47
+ controller.abort();
48
+ },
43
49
  });
44
50
  }
45
51
 
@@ -3,9 +3,8 @@ import type {
3
3
  Context,
4
4
  ServiceInfo,
5
5
  ServiceProvider,
6
- ShutdownService,
7
6
  } from "@flowscripter/dynamic-cli-framework-api";
8
- import { FETCH_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
7
+ import { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
9
8
  import DefaultFetchService from "./DefaultFetchService.ts";
10
9
 
11
10
  export default class FetchServiceProvider implements ServiceProvider {
@@ -26,8 +25,7 @@ export default class FetchServiceProvider implements ServiceProvider {
26
25
  }
27
26
 
28
27
  public initService(context: Context): Promise<void> {
29
- const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
30
- this.#fetchService!.setDependencies(shutdownService);
28
+ this.#fetchService!.setContext(context);
31
29
  return Promise.resolve();
32
30
  }
33
31
  }
@@ -33,19 +33,23 @@ export default class PrinterServiceProvider implements ServiceProvider {
33
33
  return Promise.resolve({
34
34
  service: this.printerService,
35
35
  commands: [
36
- new NoHyperlinksCommand(this, this.servicePriority + 3),
37
- new DarkModeCommand(this, this.servicePriority + 2),
38
- new NoColorCommand(this, this.servicePriority + 1),
39
- new LogLevelCommand(this, this.servicePriority),
36
+ new NoHyperlinksCommand(this.servicePriority + 3),
37
+ new DarkModeCommand(this.servicePriority + 2),
38
+ new NoColorCommand(this.servicePriority + 1),
39
+ new LogLevelCommand(this.servicePriority),
40
40
  ],
41
41
  });
42
42
  }
43
43
 
44
44
  initService(context: Context): Promise<void> {
45
45
  const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
46
- shutdownService.addShutdownListener(async () => {
47
- await this.printerService.hideSpinner();
48
- await this.printerService.hideAllProgressBars();
46
+ shutdownService.registerTask({
47
+ id: PRINTER_SERVICE_ID,
48
+ priority: 0,
49
+ run: async () => {
50
+ await this.printerService.hideSpinner();
51
+ await this.printerService.hideAllProgressBars();
52
+ },
49
53
  });
50
54
 
51
55
  return Promise.resolve();
@@ -2,10 +2,11 @@ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-
2
2
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type PrinterServiceProvider from "../PrinterServiceProvider.ts";
5
+ import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
 
7
8
  /**
8
- * Command allowing the specification of dark/light mode for {@link PrinterServiceProvider}.
9
+ * Command allowing the specification of dark/light mode for the {@link PrinterService}.
9
10
  */
10
11
  export default class DarkModeCommand implements GlobalModifierCommand {
11
12
  readonly name = "dark-mode";
@@ -18,15 +19,13 @@ export default class DarkModeCommand implements GlobalModifierCommand {
18
19
  };
19
20
  readonly executePriority: number;
20
21
 
21
- readonly #printerServiceProvider: PrinterServiceProvider;
22
-
23
- public constructor(printerServiceProvider: PrinterServiceProvider, executePriority: number) {
24
- this.#printerServiceProvider = printerServiceProvider;
22
+ public constructor(executePriority: number) {
25
23
  this.executePriority = executePriority;
26
24
  }
27
25
 
28
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
29
- this.#printerServiceProvider.printerService!.darkMode = argumentValue as boolean;
26
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
27
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
28
+ printerService.darkMode = argumentValue as boolean;
30
29
 
31
30
  return Promise.resolve();
32
31
  }
@@ -2,11 +2,12 @@ import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-
2
2
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type PrinterServiceProvider from "../PrinterServiceProvider.ts";
5
+ import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
  import { Level } from "@flowscripter/dynamic-cli-framework-api";
7
8
 
8
9
  /**
9
- * Command allowing the setting of the log level for {@link PrinterServiceProvider}.
10
+ * Command allowing the setting of the log level for the {@link PrinterService}.
10
11
  */
11
12
  export default class LogLevelCommand implements GlobalModifierCommand {
12
13
  readonly name = "log-level";
@@ -21,28 +22,26 @@ export default class LogLevelCommand implements GlobalModifierCommand {
21
22
  };
22
23
  readonly executePriority: number;
23
24
 
24
- readonly #printerServiceProvider: PrinterServiceProvider;
25
-
26
- public constructor(printerServiceProvider: PrinterServiceProvider, executePriority: number) {
27
- this.#printerServiceProvider = printerServiceProvider;
25
+ public constructor(executePriority: number) {
28
26
  this.executePriority = executePriority;
29
27
  }
30
28
 
31
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
29
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
30
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
32
31
  const logLevel = argumentValue as string;
33
32
 
34
33
  switch (logLevel.toUpperCase()) {
35
34
  case "DEBUG":
36
- this.#printerServiceProvider.printerService!.setLevel(Level.DEBUG);
35
+ printerService.setLevel(Level.DEBUG);
37
36
  break;
38
37
  case "INFO":
39
- this.#printerServiceProvider.printerService!.setLevel(Level.INFO);
38
+ printerService.setLevel(Level.INFO);
40
39
  break;
41
40
  case "WARN":
42
- this.#printerServiceProvider.printerService!.setLevel(Level.WARN);
41
+ printerService.setLevel(Level.WARN);
43
42
  break;
44
43
  case "ERROR":
45
- this.#printerServiceProvider.printerService!.setLevel(Level.ERROR);
44
+ printerService.setLevel(Level.ERROR);
46
45
  break;
47
46
  }
48
47
 
@@ -2,10 +2,11 @@ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-
2
2
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type PrinterServiceProvider from "../PrinterServiceProvider.ts";
5
+ import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
 
7
8
  /**
8
- * Command allowing the disabling of color for {@link PrinterServiceProvider}.
9
+ * Command allowing the disabling of color for the {@link PrinterService}.
9
10
  */
10
11
  export default class NoColorCommand implements GlobalModifierCommand {
11
12
  readonly name = "no-color";
@@ -18,15 +19,13 @@ export default class NoColorCommand implements GlobalModifierCommand {
18
19
  };
19
20
  readonly executePriority: number;
20
21
 
21
- readonly #printerServiceProvider: PrinterServiceProvider;
22
-
23
- public constructor(printerServiceProvider: PrinterServiceProvider, executePriority: number) {
24
- this.#printerServiceProvider = printerServiceProvider;
22
+ public constructor(executePriority: number) {
25
23
  this.executePriority = executePriority;
26
24
  }
27
25
 
28
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
29
- this.#printerServiceProvider.printerService!.colorEnabled = !argumentValue as boolean;
26
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
27
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
28
+ printerService.colorEnabled = !argumentValue as boolean;
30
29
 
31
30
  return Promise.resolve();
32
31
  }
@@ -2,7 +2,8 @@ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-
2
2
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type PrinterServiceProvider from "../PrinterServiceProvider.ts";
5
+ import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
 
7
8
  export default class NoHyperlinksCommand implements GlobalModifierCommand {
8
9
  readonly name = "no-hyperlinks";
@@ -15,15 +16,13 @@ export default class NoHyperlinksCommand implements GlobalModifierCommand {
15
16
  };
16
17
  readonly executePriority: number;
17
18
 
18
- readonly #printerServiceProvider: PrinterServiceProvider;
19
-
20
- public constructor(printerServiceProvider: PrinterServiceProvider, executePriority: number) {
21
- this.#printerServiceProvider = printerServiceProvider;
19
+ public constructor(executePriority: number) {
22
20
  this.executePriority = executePriority;
23
21
  }
24
22
 
25
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
26
- this.#printerServiceProvider.printerService!.hyperlinksEnabled = !argumentValue as boolean;
23
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
24
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
25
+ printerService.hyperlinksEnabled = !argumentValue as boolean;
27
26
 
28
27
  return Promise.resolve();
29
28
  }
@@ -18,7 +18,7 @@ export default class PrompterServiceProvider implements ServiceProvider {
18
18
  public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
19
19
  return Promise.resolve({
20
20
  service: this.prompterService,
21
- commands: [new NoPromptCommand(this, this.servicePriority)],
21
+ commands: [new NoPromptCommand(this.servicePriority)],
22
22
  });
23
23
  }
24
24
 
@@ -2,7 +2,8 @@ import type { GlobalModifierCommand } from "@flowscripter/dynamic-cli-framework-
2
2
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { type SingleValueType, ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
5
- import type PrompterServiceProvider from "../PrompterServiceProvider.ts";
5
+ import type { PrompterService } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PROMPTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
 
7
8
  export default class NoPromptCommand implements GlobalModifierCommand {
8
9
  readonly name = "no-prompt";
@@ -15,15 +16,13 @@ export default class NoPromptCommand implements GlobalModifierCommand {
15
16
  };
16
17
  readonly executePriority: number;
17
18
 
18
- readonly #prompterServiceProvider: PrompterServiceProvider;
19
-
20
- public constructor(prompterServiceProvider: PrompterServiceProvider, executePriority: number) {
21
- this.#prompterServiceProvider = prompterServiceProvider;
19
+ public constructor(executePriority: number) {
22
20
  this.executePriority = executePriority;
23
21
  }
24
22
 
25
- public execute(_context: Context, argumentValue: SingleValueType): Promise<void> {
26
- this.#prompterServiceProvider.prompterService.promptEnabled = !(argumentValue as boolean);
23
+ public execute(context: Context, argumentValue: SingleValueType): Promise<void> {
24
+ const prompterService = context.getServiceById(PROMPTER_SERVICE_ID) as PrompterService;
25
+ prompterService.promptEnabled = !(argumentValue as boolean);
27
26
 
28
27
  return Promise.resolve();
29
28
  }
@@ -1,11 +1,12 @@
1
- import type { ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
1
+ import type { ShutdownService, ShutdownTask } from "@flowscripter/dynamic-cli-framework-api";
2
+ import PriorityTaskList from "../../runtime/lifecycle/PriorityTaskList.ts";
2
3
  import { shutdownState } from "./ShutdownState.ts";
3
4
 
4
5
  export default class DefaultShutdownService implements ShutdownService {
5
- static readonly callbacks: Array<() => Promise<void>> = [];
6
+ static readonly taskList = new PriorityTaskList<ShutdownTask>();
6
7
 
7
- addShutdownListener(callback: () => Promise<void>): void {
8
- DefaultShutdownService.callbacks.push(callback);
8
+ registerTask(task: ShutdownTask): void {
9
+ DefaultShutdownService.taskList.add(task);
9
10
  }
10
11
 
11
12
  enterLongRunningMode(): void {