@flowscripter/dynamic-cli-framework 3.0.1 → 4.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 (51) hide show
  1. package/dist/index.d.ts +2 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -3
  4. package/dist/src/cli/BaseCLI.d.ts +1 -0
  5. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  6. package/dist/src/cli/BaseCLI.js +8 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +4 -0
  8. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  9. package/dist/src/launcher.d.ts +0 -2
  10. package/dist/src/launcher.d.ts.map +1 -1
  11. package/dist/src/launcher.js +0 -20
  12. package/dist/src/service/completion/CompletionServiceProvider.js +4 -4
  13. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +19 -8
  14. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  15. package/dist/src/service/configuration/ConfigurationServiceProvider.js +26 -33
  16. package/dist/src/service/configuration/DefaultKeyValueService.d.ts +6 -6
  17. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  18. package/dist/src/service/configuration/DefaultKeyValueService.js +60 -19
  19. package/dist/src/service/configuration/resolveSecrets.d.ts +11 -0
  20. package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -0
  21. package/dist/src/service/configuration/resolveSecrets.js +31 -0
  22. package/dist/src/service/plugin/DefaultPluginService.d.ts +5 -2
  23. package/dist/src/service/plugin/DefaultPluginService.d.ts.map +1 -1
  24. package/dist/src/service/plugin/DefaultPluginService.js +21 -2
  25. package/dist/src/service/plugin/PluginServiceProvider.d.ts +17 -0
  26. package/dist/src/service/plugin/PluginServiceProvider.d.ts.map +1 -0
  27. package/dist/src/service/plugin/PluginServiceProvider.js +95 -0
  28. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
  29. package/dist/src/service/upgrade/DefaultUpgradeService.js +12 -10
  30. package/dist/src/service/upgrade/UpgradeServiceProvider.js +4 -4
  31. package/index.ts +2 -7
  32. package/package.json +3 -3
  33. package/src/cli/BaseCLI.ts +17 -0
  34. package/src/cli/BaseCLIFeatureOptions.ts +7 -0
  35. package/src/launcher.ts +0 -35
  36. package/src/service/completion/CompletionServiceProvider.ts +4 -4
  37. package/src/service/configuration/ConfigurationServiceProvider.ts +36 -40
  38. package/src/service/configuration/DefaultKeyValueService.ts +76 -22
  39. package/src/service/configuration/resolveSecrets.ts +35 -0
  40. package/src/service/plugin/DefaultPluginService.ts +41 -3
  41. package/src/service/plugin/PluginServiceProvider.ts +142 -0
  42. package/src/service/upgrade/DefaultUpgradeService.ts +10 -10
  43. package/src/service/upgrade/UpgradeServiceProvider.ts +4 -4
  44. package/dist/src/cli/DynamicPluginRuntimeCLI.d.ts +0 -20
  45. package/dist/src/cli/DynamicPluginRuntimeCLI.d.ts.map +0 -1
  46. package/dist/src/cli/DynamicPluginRuntimeCLI.js +0 -50
  47. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +0 -15
  48. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +0 -1
  49. package/dist/src/service/plugin/DefaultPluginServiceProvider.js +0 -56
  50. package/src/cli/DynamicPluginRuntimeCLI.ts +0 -77
  51. package/src/service/plugin/DefaultPluginServiceProvider.ts +0 -82
@@ -1,7 +1,26 @@
1
+ import { NpmPluginManager, NpmjsPluginRepository, NpmPluginRepository, } from "@flowscripter/dynamic-plugin-framework";
1
2
  export default class DefaultPluginService {
3
+ #defaultRemoteConfig;
4
+ #defaultLocalConfig;
2
5
  #pluginManager;
3
- constructor(pluginManager) {
4
- this.#pluginManager = pluginManager;
6
+ constructor(remoteConfig, localConfig) {
7
+ this.#defaultRemoteConfig = remoteConfig;
8
+ this.#defaultLocalConfig = localConfig;
9
+ this.#pluginManager = new NpmPluginManager([new NpmjsPluginRepository(remoteConfig)], new NpmPluginRepository(localConfig));
10
+ }
11
+ get pluginManager() {
12
+ return this.#pluginManager;
13
+ }
14
+ async applyKeyValueOverrides(keyValueService) {
15
+ let remotesConfig = [this.#defaultRemoteConfig];
16
+ if (await keyValueService.has("remotes-config")) {
17
+ remotesConfig = (await keyValueService.get("remotes-config"));
18
+ }
19
+ let localConfig = this.#defaultLocalConfig;
20
+ if (await keyValueService.has("local-config")) {
21
+ localConfig = (await keyValueService.get("local-config"));
22
+ }
23
+ this.#pluginManager = new NpmPluginManager(remotesConfig.map((config) => new NpmjsPluginRepository(config)), new NpmPluginRepository(localConfig));
5
24
  }
6
25
  search(query) {
7
26
  return this.#pluginManager.search(query);
@@ -0,0 +1,17 @@
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 { NpmjsPluginRepositoryConfig, NpmPluginRepositoryConfig } from "@flowscripter/dynamic-plugin-framework";
5
+ import { type SpawnInterfaceAdapterOptions } from "./SpawnInterfaceAdapter.ts";
6
+ import { type FetchInterfaceAdapterOptions } from "./FetchInterfaceAdapter.ts";
7
+ import DefaultCommandRegistry from "../../runtime/registry/DefaultCommandRegistry.ts";
8
+ import DefaultServiceProviderRegistry from "../../runtime/registry/DefaultServiceProviderRegistry.ts";
9
+ export default class PluginServiceProvider implements ServiceProvider {
10
+ #private;
11
+ readonly serviceId = "PLUGIN_SERVICE";
12
+ readonly servicePriority: number;
13
+ constructor(servicePriority: number, remoteConfig: NpmjsPluginRepositoryConfig, localConfig: NpmPluginRepositoryConfig, commandRegistry: DefaultCommandRegistry, serviceProviderRegistry: DefaultServiceProviderRegistry, spawnInterfaceAdapterOptions?: SpawnInterfaceAdapterOptions, fetchInterfaceAdapterOptions?: FetchInterfaceAdapterOptions);
14
+ getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo>;
15
+ initService(context: Context): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=PluginServiceProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PluginServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/plugin/PluginServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAWzE,OAAO,KAAK,EAEV,2BAA2B,EAC3B,yBAAyB,EAE1B,MAAM,wCAAwC,CAAC;AAIhD,OAA8B,EAC5B,KAAK,4BAA4B,EAClC,MAAM,4BAA4B,CAAC;AACpC,OAA8B,EAC5B,KAAK,4BAA4B,EAClC,MAAM,4BAA4B,CAAC;AACpC,OAAO,sBAAsB,MAAM,kDAAkD,CAAC;AACtF,OAAO,8BAA8B,MAAM,0DAA0D,CAAC;AAMtG,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,eAAe;;IACnE,QAAQ,CAAC,SAAS,oBAAqB;IACvC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;gBAW/B,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,2BAA2B,EACzC,WAAW,EAAE,yBAAyB,EACtC,eAAe,EAAE,sBAAsB,EACvC,uBAAuB,EAAE,8BAA8B,EACvD,4BAA4B,GAAE,4BAAiC,EAC/D,4BAA4B,GAAE,4BAAiC;IAW3D,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAS1D,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAmEnD"}
@@ -0,0 +1,95 @@
1
+ import { PRINTER_SERVICE_ID, SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
+ import { KEY_VALUE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
4
+ import { DYNAMIC_CLI_FRAMEWORK_COMMAND_FACTORY_EXTENSION_POINT } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { DYNAMIC_CLI_FRAMEWORK_SERVICE_PROVIDER_FACTORY_EXTENSION_POINT } from "@flowscripter/dynamic-cli-framework-api";
6
+ import { PLUGIN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
7
+ import DefaultPluginService from "./DefaultPluginService.js";
8
+ import { PluginGroupCommand } from "./command/PluginGroupCommand.js";
9
+ import SpawnInterfaceAdapter, {} from "./SpawnInterfaceAdapter.js";
10
+ import FetchInterfaceAdapter, {} from "./FetchInterfaceAdapter.js";
11
+ import DefaultCommandRegistry from "../../runtime/registry/DefaultCommandRegistry.js";
12
+ import DefaultServiceProviderRegistry from "../../runtime/registry/DefaultServiceProviderRegistry.js";
13
+ import getLogger from "../../util/logger.js";
14
+ const logger = getLogger("PluginServiceProvider");
15
+ export default class PluginServiceProvider {
16
+ serviceId = PLUGIN_SERVICE_ID;
17
+ servicePriority;
18
+ #remoteConfig;
19
+ #localConfig;
20
+ #commandRegistry;
21
+ #serviceProviderRegistry;
22
+ #spawnInterfaceAdapterOptions;
23
+ #fetchInterfaceAdapterOptions;
24
+ #pluginService;
25
+ #cliConfig;
26
+ constructor(servicePriority, remoteConfig, localConfig, commandRegistry, serviceProviderRegistry, spawnInterfaceAdapterOptions = {}, fetchInterfaceAdapterOptions = {}) {
27
+ this.servicePriority = servicePriority;
28
+ this.#remoteConfig = remoteConfig;
29
+ this.#localConfig = localConfig;
30
+ this.#commandRegistry = commandRegistry;
31
+ this.#serviceProviderRegistry = serviceProviderRegistry;
32
+ this.#spawnInterfaceAdapterOptions = spawnInterfaceAdapterOptions;
33
+ this.#fetchInterfaceAdapterOptions = fetchInterfaceAdapterOptions;
34
+ }
35
+ async getServiceInfo(cliConfig) {
36
+ this.#cliConfig = cliConfig;
37
+ this.#pluginService = new DefaultPluginService(this.#remoteConfig, this.#localConfig);
38
+ return {
39
+ service: this.#pluginService,
40
+ commands: [new PluginGroupCommand()],
41
+ };
42
+ }
43
+ async initService(context) {
44
+ const pluginService = this.#pluginService;
45
+ if (context.doesServiceExist(KEY_VALUE_SERVICE_ID)) {
46
+ const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID);
47
+ await pluginService.applyKeyValueOverrides(keyValueService);
48
+ }
49
+ const pluginManager = pluginService.pluginManager;
50
+ if (!context.doesServiceExist(SPAWN_SERVICE_ID)) {
51
+ logger.debug("SpawnService not available, plugin manager will spawn processes directly");
52
+ }
53
+ else if (!("setSpawn" in pluginManager)) {
54
+ logger.debug("Plugin manager does not support SpawnCapable, skipping spawn injection");
55
+ }
56
+ else {
57
+ const spawnService = context.getServiceById(SPAWN_SERVICE_ID);
58
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
59
+ const adapter = new SpawnInterfaceAdapter(spawnService, printerService, this.#spawnInterfaceAdapterOptions);
60
+ pluginManager.setSpawn(adapter);
61
+ }
62
+ if (!context.doesServiceExist(FETCH_SERVICE_ID)) {
63
+ logger.debug("FetchService not available, plugin manager will fetch directly");
64
+ }
65
+ else if (!("setFetch" in pluginManager)) {
66
+ logger.debug("Plugin manager does not support FetchCapable, skipping fetch injection");
67
+ }
68
+ else {
69
+ const fetchService = context.getServiceById(FETCH_SERVICE_ID);
70
+ const adapter = new FetchInterfaceAdapter(fetchService, this.#fetchInterfaceAdapterOptions);
71
+ pluginManager.setFetch(adapter);
72
+ }
73
+ await pluginManager.registerExtensions(DYNAMIC_CLI_FRAMEWORK_COMMAND_FACTORY_EXTENSION_POINT);
74
+ await pluginManager.registerExtensions(DYNAMIC_CLI_FRAMEWORK_SERVICE_PROVIDER_FACTORY_EXTENSION_POINT);
75
+ const commandFactoryExtensions = await pluginManager.getRegisteredExtensions(DYNAMIC_CLI_FRAMEWORK_COMMAND_FACTORY_EXTENSION_POINT);
76
+ for (const ext of commandFactoryExtensions) {
77
+ const factory = (await pluginManager.instantiate(ext.extensionHandle));
78
+ for (const command of factory.getCommands()) {
79
+ this.#commandRegistry.addCommand(command);
80
+ }
81
+ }
82
+ const spFactoryExtensions = await pluginManager.getRegisteredExtensions(DYNAMIC_CLI_FRAMEWORK_SERVICE_PROVIDER_FACTORY_EXTENSION_POINT);
83
+ for (const ext of spFactoryExtensions) {
84
+ const factory = (await pluginManager.instantiate(ext.extensionHandle));
85
+ for (const sp of factory.getServiceProviders()) {
86
+ const info = await sp.getServiceInfo(this.#cliConfig);
87
+ if (info.service) {
88
+ context.addServiceInstance(sp.serviceId, info.service);
89
+ }
90
+ info.commands.forEach((cmd) => this.#commandRegistry.addCommand(cmd, sp.serviceId));
91
+ this.#serviceProviderRegistry.addServiceProvider(sp);
92
+ }
93
+ }
94
+ }
95
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EAEZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAO1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAc5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAO/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC,IAAI;IAKA,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAyBrF,QAAQ,IAAI,WAAW,GAAG,SAAS;IAanC,UAAU,IAAI,aAAa,GAAG,SAAS;IAWjC,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAgBxE,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAmC7B,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CAuQ1B"}
1
+ {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EAEZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAS1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAc5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAO/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC,IAAI;IAKA,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IA0BrF,QAAQ,IAAI,WAAW,GAAG,SAAS;IAanC,UAAU,IAAI,aAAa,GAAG,SAAS;IAWjC,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAgBxE,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAmC7B,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CAoQ1B"}
@@ -7,7 +7,9 @@ import semver from "semver";
7
7
  import getLogger from "../../util/logger.js";
8
8
  const logger = getLogger("DefaultUpgradeService");
9
9
  // checkForUpgrade() runs opportunistically on every CLI invocation (via BannerServiceProvider),
10
- // so its version-check network/spawn calls must never be allowed to stall CLI startup.
10
+ // so that opportunistic call is raced against this timeout so it never stalls CLI startup.
11
+ // A deliberate caller (waitForResult=true, e.g. the `upgrade` command) awaits checkForUpgrade()
12
+ // directly with no timeout, so its underlying network/spawn calls are never artificially cut off.
11
13
  export const VERSION_CHECK_TIMEOUT_MS = 250;
12
14
  function describeSpawnFailure(result) {
13
15
  return "timedOut" in result
@@ -36,10 +38,11 @@ export default class DefaultUpgradeService {
36
38
  getUpgradeCheckResult(waitForResult = false) {
37
39
  if (!this.#upgradeCheckPromise) {
38
40
  // Only cache a successful result. An `undefined` result can come from a transient
39
- // failure (e.g. the opportunistic startup check's fetch exceeding
40
- // VERSION_CHECK_TIMEOUT_MS) - caching that would permanently deny a later, deliberate
41
- // caller (e.g. the `upgrade` command explicitly waiting via waitForResult=true) any
42
- // chance of a fresh attempt for the rest of this process's lifetime.
41
+ // failure (e.g. the opportunistic startup check racing past VERSION_CHECK_TIMEOUT_MS
42
+ // before checkForUpgrade() itself resolves) - caching that would permanently deny a
43
+ // later, deliberate caller (e.g. the `upgrade` command explicitly waiting via
44
+ // waitForResult=true) any chance of a fresh attempt for the rest of this process's
45
+ // lifetime.
43
46
  this.#upgradeCheckPromise = this.checkForUpgrade().then((result) => {
44
47
  if (result === undefined) {
45
48
  this.#upgradeCheckPromise = undefined;
@@ -169,14 +172,14 @@ export default class DefaultUpgradeService {
169
172
  if (!this.#spawnService || !this.#config.homebrew) {
170
173
  return false;
171
174
  }
172
- const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
175
+ const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { mode: "ignore", longRunning: false });
173
176
  return result.ok;
174
177
  }
175
178
  async #isWingetInstalled() {
176
179
  if (!this.#spawnService || !this.#config.winget) {
177
180
  return false;
178
181
  }
179
- const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
182
+ const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { mode: "ignore", longRunning: false });
180
183
  return result.ok;
181
184
  }
182
185
  #isLinuxScriptInstall() {
@@ -201,7 +204,7 @@ export default class DefaultUpgradeService {
201
204
  }
202
205
  const { owner, repo } = this.#config.githubRelease;
203
206
  try {
204
- const response = await this.#fetchService.fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS });
207
+ const response = await this.#fetchService.fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
205
208
  if (!response.ok) {
206
209
  return undefined;
207
210
  }
@@ -223,7 +226,7 @@ export default class DefaultUpgradeService {
223
226
  return undefined;
224
227
  }
225
228
  try {
226
- const response = await this.#fetchService.fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS });
229
+ const response = await this.#fetchService.fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`);
227
230
  if (!response.ok) {
228
231
  return undefined;
229
232
  }
@@ -243,7 +246,6 @@ export default class DefaultUpgradeService {
243
246
  const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
244
247
  mode: "wrapped",
245
248
  longRunning: false,
246
- timeoutMs: VERSION_CHECK_TIMEOUT_MS,
247
249
  onOutput: (line) => lines.push(line),
248
250
  });
249
251
  if (!result.ok) {
@@ -52,8 +52,8 @@ export default class UpgradeServiceProvider {
52
52
  return;
53
53
  }
54
54
  const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID);
55
- if (await keyValueService.hasKey("upgrade-status")) {
56
- const status = await keyValueService.getKey("upgrade-status");
55
+ if (await keyValueService.has("upgrade-status")) {
56
+ const status = await keyValueService.get("upgrade-status");
57
57
  if (status === "declined") {
58
58
  logger.debug(() => "Auto-upgrade previously declined, skipping");
59
59
  return;
@@ -97,10 +97,10 @@ export default class UpgradeServiceProvider {
97
97
  return;
98
98
  }
99
99
  if (enableResult.value !== true) {
100
- await keyValueService.setKey("upgrade-status", "declined");
100
+ await keyValueService.set("upgrade-status", "declined");
101
101
  return;
102
102
  }
103
- await keyValueService.setKey("upgrade-status", "enabled");
103
+ await keyValueService.set("upgrade-status", "enabled");
104
104
  await this.#checkAndUpgrade(context, upgradeService, cliConfig);
105
105
  }
106
106
  async #checkAndUpgrade(context, upgradeService, cliConfig) {
package/index.ts CHANGED
@@ -187,12 +187,11 @@ export type { ServiceProviderFactory } from "@flowscripter/dynamic-cli-framework
187
187
  export { DYNAMIC_CLI_FRAMEWORK_SERVICE_PROVIDER_FACTORY_EXTENSION_POINT } from "@flowscripter/dynamic-cli-framework-api";
188
188
  export { PLUGIN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
189
189
  export type { PluginService } from "@flowscripter/dynamic-cli-framework-api";
190
- export { default as DefaultPluginServiceProvider } from "./src/service/plugin/DefaultPluginServiceProvider.ts";
190
+ export { default as PluginServiceProvider } from "./src/service/plugin/PluginServiceProvider.ts";
191
191
 
192
192
  // Core CLI
193
193
  export { default as BaseCLI } from "./src/cli/BaseCLI.ts";
194
194
  export { default as DefaultRuntimeCLI } from "./src/cli/DefaultRuntimeCLI.ts";
195
- export { default as DynamicPluginRuntimeCLI } from "./src/cli/DynamicPluginRuntimeCLI.ts";
196
195
 
197
196
  // Terminal API
198
197
  export type { default as Terminal } from "./src/terminal/Terminal.ts";
@@ -202,8 +201,4 @@ export { SpecialKey } from "./src/terminal/KeyReader.ts";
202
201
  export { default as TtyKeyReader } from "./src/terminal/TtyKeyReader.ts";
203
202
 
204
203
  // Convenience functions
205
- export {
206
- launchDynamicPluginMultiCommandCLI,
207
- launchMultiCommandCLI,
208
- launchSingleCommandCLI,
209
- } from "./src/launcher.ts";
204
+ export { launchMultiCommandCLI, launchSingleCommandCLI } from "./src/launcher.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "3.0.1",
3
+ "version": "4.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,8 +48,8 @@
48
48
  "build": "tsc -p tsconfig.build.json"
49
49
  },
50
50
  "dependencies": {
51
- "@flowscripter/dynamic-cli-framework-api": "^1.4.1",
52
- "@flowscripter/dynamic-plugin-framework": "1.12.0",
51
+ "@flowscripter/dynamic-cli-framework-api": "^1.5.0",
52
+ "@flowscripter/dynamic-plugin-framework": "2.0.0",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
55
55
  "figlet": "^1.11.0",
@@ -54,6 +54,7 @@ import ImagePrinterServiceProvider from "../service/imagePrinter/ImagePrinterSer
54
54
  import SpawnServiceProvider from "../service/spawn/SpawnServiceProvider.ts";
55
55
  import FetchServiceProvider from "../service/fetch/FetchServiceProvider.ts";
56
56
  import UpgradeServiceProvider from "../service/upgrade/UpgradeServiceProvider.ts";
57
+ import PluginServiceProvider from "../service/plugin/PluginServiceProvider.ts";
57
58
  const logger = getLogger("BaseCLI");
58
59
 
59
60
  /**
@@ -76,6 +77,7 @@ const logger = getLogger("BaseCLI");
76
77
  * * {@link FetchServiceProvider}
77
78
  * * {@link CompletionServiceProvider}
78
79
  * * {@link UpgradeServiceProvider}
80
+ * * {@link PluginServiceProvider}
79
81
  *
80
82
  * `keyReader` is optional. If omitted, or if the stderr {@link Terminal} is not a TTY, prompting is
81
83
  * unavailable: {@link PrompterServiceProvider} (and {@link ArgumentPrompterServiceProvider}, if
@@ -141,6 +143,9 @@ export default class BaseCLI implements CLI {
141
143
  fetchServiceEnabled: false,
142
144
  upgradeServiceEnabled: false,
143
145
  upgradeLocationsConfig: { supportedPlatforms: [] },
146
+ pluginServiceEnabled: false,
147
+ pluginServiceRemoteConfig: { name: "", registryUrl: "", packageJsonNamespace: "" },
148
+ pluginServiceLocalConfig: { nodeModulesPath: "", packageJsonNamespace: "" },
144
149
  validateAllCommands: false,
145
150
  promptingEnabled: true,
146
151
  ...options,
@@ -292,6 +297,18 @@ export default class BaseCLI implements CLI {
292
297
  this.addServiceProvider(new UpgradeServiceProvider(6, this.#options.upgradeLocationsConfig));
293
298
  }
294
299
 
300
+ if (this.#options.pluginServiceEnabled) {
301
+ this.addServiceProvider(
302
+ new PluginServiceProvider(
303
+ 50,
304
+ this.#options.pluginServiceRemoteConfig,
305
+ this.#options.pluginServiceLocalConfig,
306
+ this.#commandRegistry,
307
+ this.#serviceProviderRegistry,
308
+ ),
309
+ );
310
+ }
311
+
295
312
  const configurationServiceProvider = new ConfigurationServiceProvider(
296
313
  90,
297
314
  this.#options.envVarsSupportEnabled,
@@ -1,4 +1,8 @@
1
1
  import type { UpgradeLocationsConfig } from "../service/upgrade/UpgradeLocationsConfig.ts";
2
+ import type {
3
+ NpmjsPluginRepositoryConfig,
4
+ NpmPluginRepositoryConfig,
5
+ } from "@flowscripter/dynamic-plugin-framework";
2
6
 
3
7
  export default interface BaseCLIFeatureOptions {
4
8
  readonly configFileSupportEnabled?: boolean;
@@ -12,6 +16,9 @@ export default interface BaseCLIFeatureOptions {
12
16
  readonly fetchServiceEnabled?: boolean;
13
17
  readonly upgradeServiceEnabled?: boolean;
14
18
  readonly upgradeLocationsConfig?: UpgradeLocationsConfig;
19
+ readonly pluginServiceEnabled?: boolean;
20
+ readonly pluginServiceRemoteConfig?: NpmjsPluginRepositoryConfig;
21
+ readonly pluginServiceLocalConfig?: NpmPluginRepositoryConfig;
15
22
  readonly validateAllCommands?: boolean;
16
23
  readonly promptingEnabled?: boolean;
17
24
  }
package/src/launcher.ts CHANGED
@@ -9,8 +9,6 @@ import type {
9
9
  } from "@flowscripter/dynamic-cli-framework-api";
10
10
  import type BaseCLIFeatureOptions from "./cli/BaseCLIFeatureOptions.ts";
11
11
  import DefaultRuntimeCLI from "./cli/DefaultRuntimeCLI.ts";
12
- import DynamicPluginRuntimeCLI from "./cli/DynamicPluginRuntimeCLI.ts";
13
- import type { MarketplacePluginManager } from "@flowscripter/dynamic-plugin-framework";
14
12
 
15
13
  export async function launchSingleCommandCLI(
16
14
  command: SubCommand,
@@ -75,36 +73,3 @@ export async function launchMultiCommandCLI(
75
73
 
76
74
  return await cli.run();
77
75
  }
78
-
79
- export async function launchDynamicPluginMultiCommandCLI(
80
- pluginManager: MarketplacePluginManager,
81
- commands: ReadonlyArray<Command>,
82
- description?: string,
83
- name?: string,
84
- version?: string,
85
- serviceProviders?: ReadonlyArray<ServiceProvider>,
86
- options?: BaseCLIFeatureOptions,
87
- ): Promise<RunResult> {
88
- if (!name) {
89
- name = path.basename(process.execPath);
90
- }
91
- const cliConfig: CLIConfig = {
92
- description,
93
- name,
94
- version: version || "N/A",
95
- };
96
-
97
- const mergedOptions: BaseCLIFeatureOptions = {
98
- ...options,
99
- validateAllCommands:
100
- (options?.validateAllCommands ?? false) ||
101
- process.env.DYNAMIC_CLI_FRAMEWORK_VALIDATE_ALL !== undefined,
102
- };
103
- const cli = new DynamicPluginRuntimeCLI(cliConfig, pluginManager, mergedOptions);
104
-
105
- commands.forEach((command) => cli.addCommand(command));
106
-
107
- serviceProviders?.forEach((service) => cli.addServiceProvider(service));
108
-
109
- return await cli.run();
110
- }
@@ -61,8 +61,8 @@ export default class CompletionServiceProvider implements ServiceProvider {
61
61
  }
62
62
 
63
63
  const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID) as KeyValueService;
64
- if (await keyValueService.hasKey("completion-status")) {
65
- const status = await keyValueService.getKey("completion-status");
64
+ if (await keyValueService.has("completion-status")) {
65
+ const status = await keyValueService.get("completion-status");
66
66
  if (status === "installed" || status === "declined") {
67
67
  logger.debug(() => `Completion status is '${status}', skipping auto-prompt`);
68
68
  return;
@@ -99,7 +99,7 @@ export default class CompletionServiceProvider implements ServiceProvider {
99
99
  }
100
100
 
101
101
  if (enableResult.value !== true) {
102
- await keyValueService.setKey("completion-status", "declined");
102
+ await keyValueService.set("completion-status", "declined");
103
103
  return;
104
104
  }
105
105
 
@@ -131,7 +131,7 @@ export default class CompletionServiceProvider implements ServiceProvider {
131
131
 
132
132
  try {
133
133
  await this.#installCompletion(context, shellType);
134
- await keyValueService.setKey("completion-status", "installed");
134
+ await keyValueService.set("completion-status", "installed");
135
135
  } catch (error) {
136
136
  if (context.doesServiceExist(PRINTER_SERVICE_ID)) {
137
137
  const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
@@ -7,6 +7,7 @@ import ConfigCommand from "./command/ConfigCommand.ts";
7
7
  import type {
8
8
  SingleValueType,
9
9
  Values,
10
+ ValueNode,
10
11
  PopulatedValues,
11
12
  PopulatedValueType,
12
13
  } from "@flowscripter/dynamic-cli-framework-api";
@@ -19,6 +20,7 @@ import {
19
20
  } from "@flowscripter/dynamic-cli-framework-api";
20
21
  import DefaultKeyValueService from "./DefaultKeyValueService.ts";
21
22
  import DefaultSecretService from "./DefaultSecretService.ts";
23
+ import resolveSecrets from "./resolveSecrets.ts";
22
24
  import type { Command } from "@flowscripter/dynamic-cli-framework-api";
23
25
  import argumentValueMerge from "../../runtime/values/argumentValueMerge.ts";
24
26
  import {
@@ -129,13 +131,21 @@ const logger = getLogger("ConfigurationServiceProvider");
129
131
  *
130
132
  * The second and third level of properties is used to scope the key-values to specific command names
131
133
  * (via {@link Command.name} values) and specific service IDs (via {@link ServiceProvider.serviceId} values).
132
- * Both keys and values are string based.
134
+ * Keys are strings, but values are arbitrary JSON (see {@link ValueNode}) - not limited to strings -
135
+ * and may be deep objects or arrays.
133
136
  *
134
- * Values may be stored as OS-native secrets via {@link KeyValueService.setKey} with `isSecret=true`.
135
- * When a value is stored as a secret, a sentinel value of the format `__SECRET__:<bun_secret_name>`
136
- * is stored in the config file instead of the actual value. The actual value is stored in the OS
137
- * credential store via Bun.secrets. The sentinel prefix `__SECRET__:` is reserved and must not be
138
- * used for regular key-value data.
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.
139
149
  *
140
150
  * Secret support requires `secretServiceEnabled=true` in the constructor (which also requires
141
151
  * `configEnabled=true`).
@@ -150,7 +160,10 @@ const logger = getLogger("ConfigurationServiceProvider");
150
160
  * "commands": {
151
161
  * "command1": {
152
162
  * "foo1": "bar1",
153
- * "foo2": "__SECRET__:command_command1_foo2"
163
+ * "foo2": "__SECRET__:command_command1_foo2",
164
+ * "foo3": {
165
+ * "nested": ["a", "__SECRET__:command_command1_foo3_nested"]
166
+ * }
154
167
  * },
155
168
  * "command2": {
156
169
  * "foo1": "bar3"
@@ -184,11 +197,11 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
184
197
 
185
198
  // the configuration data to be used by the CLI runner implementation
186
199
  // when updating command scoped access to key-value data via the key-value service.
187
- #commandKeyValueData = new Map<string, Map<string, string>>();
200
+ #commandKeyValueData = new Map<string, Map<string, ValueNode>>();
188
201
 
189
202
  // the configuration data to be used by the CLI runner implementation
190
203
  // when updating service scoped access to key-value data via the key-value service.
191
- #serviceKeyValueData = new Map<string, Map<string, string>>();
204
+ #serviceKeyValueData = new Map<string, Map<string, ValueNode>>();
192
205
 
193
206
  // the optional service providing scope limited key-value data to commands and other services
194
207
  #defaultKeyValueService: DefaultKeyValueService | undefined;
@@ -301,40 +314,23 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
301
314
  }
302
315
 
303
316
  if (result !== undefined && this.#defaultSecretService) {
304
- result = await this.#resolveSecrets(result);
317
+ result = await resolveSecrets<PopulatedValues | PopulatedValueType>(
318
+ result,
319
+ async (bunSecretName) => {
320
+ const secretValue = await this.#defaultSecretService!.getSecret(bunSecretName);
321
+ if (secretValue === null) {
322
+ throw new Error(
323
+ `Secret not found in OS secret store for sentinel: '${SECRET_SENTINEL_PREFIX}${bunSecretName}'`,
324
+ );
325
+ }
326
+ return secretValue;
327
+ },
328
+ );
305
329
  }
306
330
 
307
331
  return result;
308
332
  }
309
333
 
310
- async #resolveSecrets(
311
- value: PopulatedValues | PopulatedValueType,
312
- ): Promise<PopulatedValues | PopulatedValueType> {
313
- if (typeof value === "string" && value.startsWith(SECRET_SENTINEL_PREFIX)) {
314
- const bunSecretName = value.slice(SECRET_SENTINEL_PREFIX.length);
315
- const secretValue = await this.#defaultSecretService!.getSecret(bunSecretName);
316
- if (secretValue === null) {
317
- throw new Error(`Secret not found in OS secret store for sentinel: '${value}'`);
318
- }
319
- return secretValue;
320
- }
321
- if (typeof value === "object" && value !== null) {
322
- if (Array.isArray(value)) {
323
- const resolved = [];
324
- for (const item of value) {
325
- resolved.push(await this.#resolveSecrets(item));
326
- }
327
- return resolved as unknown as PopulatedValues;
328
- }
329
- const resolved: Record<string, unknown> = {};
330
- for (const [key, val] of Object.entries(value)) {
331
- resolved[key] = await this.#resolveSecrets(val as PopulatedValueType);
332
- }
333
- return resolved as unknown as PopulatedValues;
334
- }
335
- return value;
336
- }
337
-
338
334
  /**
339
335
  * Indicate that the contained {@link DefaultKeyValueService} should be set to have a scope of the provided
340
336
  * command name.
@@ -542,8 +538,8 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
542
538
  const config: {
543
539
  defaults?: Record<string, Values | SingleValueType>;
544
540
  "key-values"?: {
545
- commands?: Record<string, Record<string, string>>;
546
- services?: Record<string, Record<string, string>>;
541
+ commands?: Record<string, Record<string, ValueNode>>;
542
+ services?: Record<string, Record<string, ValueNode>>;
547
543
  };
548
544
  } = {};
549
545