@flowscripter/dynamic-cli-framework 3.0.2 → 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.
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/src/cli/BaseCLI.d.ts +1 -0
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +8 -0
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts +4 -0
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
- package/dist/src/launcher.d.ts +0 -2
- package/dist/src/launcher.d.ts.map +1 -1
- package/dist/src/launcher.js +0 -20
- package/dist/src/service/completion/CompletionServiceProvider.js +4 -4
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +19 -8
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
- package/dist/src/service/configuration/ConfigurationServiceProvider.js +26 -33
- package/dist/src/service/configuration/DefaultKeyValueService.d.ts +6 -6
- package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
- package/dist/src/service/configuration/DefaultKeyValueService.js +60 -19
- package/dist/src/service/configuration/resolveSecrets.d.ts +11 -0
- package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -0
- package/dist/src/service/configuration/resolveSecrets.js +31 -0
- package/dist/src/service/plugin/DefaultPluginService.d.ts +5 -2
- package/dist/src/service/plugin/DefaultPluginService.d.ts.map +1 -1
- package/dist/src/service/plugin/DefaultPluginService.js +21 -2
- package/dist/src/service/plugin/PluginServiceProvider.d.ts +17 -0
- package/dist/src/service/plugin/PluginServiceProvider.d.ts.map +1 -0
- package/dist/src/service/plugin/PluginServiceProvider.js +95 -0
- package/dist/src/service/upgrade/UpgradeServiceProvider.js +4 -4
- package/index.ts +2 -7
- package/package.json +3 -3
- package/src/cli/BaseCLI.ts +17 -0
- package/src/cli/BaseCLIFeatureOptions.ts +7 -0
- package/src/launcher.ts +0 -35
- package/src/service/completion/CompletionServiceProvider.ts +4 -4
- package/src/service/configuration/ConfigurationServiceProvider.ts +36 -40
- package/src/service/configuration/DefaultKeyValueService.ts +76 -22
- package/src/service/configuration/resolveSecrets.ts +35 -0
- package/src/service/plugin/DefaultPluginService.ts +41 -3
- package/src/service/plugin/PluginServiceProvider.ts +142 -0
- package/src/service/upgrade/UpgradeServiceProvider.ts +4 -4
- package/dist/src/cli/DynamicPluginRuntimeCLI.d.ts +0 -20
- package/dist/src/cli/DynamicPluginRuntimeCLI.d.ts.map +0 -1
- package/dist/src/cli/DynamicPluginRuntimeCLI.js +0 -50
- package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +0 -15
- package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +0 -1
- package/dist/src/service/plugin/DefaultPluginServiceProvider.js +0 -56
- package/src/cli/DynamicPluginRuntimeCLI.ts +0 -77
- 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(
|
|
4
|
-
this.#
|
|
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
|
+
}
|
|
@@ -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.
|
|
56
|
-
const status = await keyValueService.
|
|
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.
|
|
100
|
+
await keyValueService.set("upgrade-status", "declined");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
await keyValueService.
|
|
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
|
|
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
|
+
"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.
|
|
52
|
-
"@flowscripter/dynamic-plugin-framework": "
|
|
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",
|
package/src/cli/BaseCLI.ts
CHANGED
|
@@ -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.
|
|
65
|
-
const status = await keyValueService.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* is
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
546
|
-
services?: Record<string, Record<string,
|
|
541
|
+
commands?: Record<string, Record<string, ValueNode>>;
|
|
542
|
+
services?: Record<string, Record<string, ValueNode>>;
|
|
547
543
|
};
|
|
548
544
|
} = {};
|
|
549
545
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ValueNode,
|
|
3
|
+
KeyValueService,
|
|
4
|
+
SettableValueNode,
|
|
5
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
6
|
+
import { Secret, SECRET_SENTINEL_PREFIX } from "@flowscripter/dynamic-cli-framework-api";
|
|
3
7
|
import type DefaultSecretService from "./DefaultSecretService.ts";
|
|
8
|
+
import resolveSecrets from "./resolveSecrets.ts";
|
|
4
9
|
|
|
5
10
|
export default class DefaultKeyValueService implements KeyValueService {
|
|
6
|
-
#keyValueData: Map<string,
|
|
11
|
+
#keyValueData: Map<string, ValueNode> | undefined;
|
|
7
12
|
#dirty = false;
|
|
8
13
|
readonly #secretService: DefaultSecretService | undefined;
|
|
9
14
|
|
|
@@ -11,7 +16,7 @@ export default class DefaultKeyValueService implements KeyValueService {
|
|
|
11
16
|
this.#secretService = secretService;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
public setKeyValueData(keyValueData: Map<string,
|
|
19
|
+
public setKeyValueData(keyValueData: Map<string, ValueNode>) {
|
|
15
20
|
if (this.#keyValueData) {
|
|
16
21
|
throw new Error("Attempt to overwrite key-value data, it should be cleared first");
|
|
17
22
|
}
|
|
@@ -28,7 +33,7 @@ export default class DefaultKeyValueService implements KeyValueService {
|
|
|
28
33
|
return this.#dirty;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
public async
|
|
36
|
+
public async get<T extends ValueNode = ValueNode>(key: string): Promise<T> {
|
|
32
37
|
if (this.#keyValueData === undefined) {
|
|
33
38
|
throw new Error("Attempt to access undefined key-value data");
|
|
34
39
|
}
|
|
@@ -37,56 +42,105 @@ export default class DefaultKeyValueService implements KeyValueService {
|
|
|
37
42
|
throw new Error("Attempt to access unknown key");
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
const resolveSecret = async (bunSecretName: string): Promise<unknown> => {
|
|
41
46
|
if (!this.#secretService) {
|
|
42
47
|
throw new Error("Secret sentinel found but no secret service is available");
|
|
43
48
|
}
|
|
44
|
-
const bunSecretName = value.slice(SECRET_SENTINEL_PREFIX.length);
|
|
45
49
|
const secretValue = await this.#secretService.getSecret(bunSecretName);
|
|
46
50
|
if (secretValue === null) {
|
|
47
51
|
throw new Error(`Secret not found in OS secret store for key: '${key}'`);
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(secretValue);
|
|
55
|
+
} catch {
|
|
56
|
+
return secretValue;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
51
59
|
|
|
52
|
-
return value
|
|
60
|
+
return resolveSecrets(value, resolveSecret) as Promise<T>;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
public
|
|
63
|
+
public has(key: string): Promise<boolean> {
|
|
56
64
|
if (this.#keyValueData === undefined) {
|
|
57
65
|
return Promise.reject(new Error("Attempt to access undefined key-value data"));
|
|
58
66
|
}
|
|
59
67
|
return Promise.resolve(this.#keyValueData.has(key));
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
public async
|
|
70
|
+
public async set(key: string, value: SettableValueNode): Promise<void> {
|
|
63
71
|
if (this.#keyValueData === undefined) {
|
|
64
72
|
throw new Error("Attempt to access undefined key-value data");
|
|
65
73
|
}
|
|
66
|
-
|
|
74
|
+
this.#keyValueData.set(key, await this.#storeSecrets(key, value, []));
|
|
75
|
+
this.#dirty = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async #storeSecrets(
|
|
79
|
+
key: string,
|
|
80
|
+
value: SettableValueNode,
|
|
81
|
+
path: Array<string>,
|
|
82
|
+
): Promise<ValueNode> {
|
|
83
|
+
if (value instanceof Secret) {
|
|
67
84
|
if (!this.#secretService) {
|
|
68
85
|
throw new Error("Attempt to set a secret but no secret service is available");
|
|
69
86
|
}
|
|
70
|
-
const
|
|
71
|
-
this.#
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
const secretName = [key, ...path].join("_");
|
|
88
|
+
const bunSecretName = await this.#secretService.setSecret(
|
|
89
|
+
secretName,
|
|
90
|
+
JSON.stringify(value.value),
|
|
91
|
+
);
|
|
92
|
+
return SECRET_SENTINEL_PREFIX + bunSecretName;
|
|
74
93
|
}
|
|
75
|
-
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
const result: Array<ValueNode> = [];
|
|
96
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
97
|
+
result.push(await this.#storeSecrets(key, value[i]!, [...path, String(i)]));
|
|
98
|
+
}
|
|
99
|
+
return result as ValueNode;
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === "object" && value !== null) {
|
|
102
|
+
const result: Record<string, ValueNode> = {};
|
|
103
|
+
for (const [propertyName, propertyValue] of Object.entries(value)) {
|
|
104
|
+
result[propertyName] = await this.#storeSecrets(key, propertyValue, [
|
|
105
|
+
...path,
|
|
106
|
+
propertyName,
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
return result as ValueNode;
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
76
112
|
}
|
|
77
113
|
|
|
78
|
-
public async
|
|
114
|
+
public async delete(key: string): Promise<void> {
|
|
79
115
|
if (this.#keyValueData === undefined) {
|
|
80
116
|
throw new Error("Attempt to access undefined key-value data");
|
|
81
117
|
}
|
|
82
118
|
const value = this.#keyValueData.get(key);
|
|
83
|
-
if (value !== undefined
|
|
119
|
+
if (value !== undefined) {
|
|
120
|
+
await this.#deleteSecrets(value);
|
|
121
|
+
}
|
|
122
|
+
this.#keyValueData.delete(key);
|
|
123
|
+
this.#dirty = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #deleteSecrets(value: ValueNode): Promise<void> {
|
|
127
|
+
if (typeof value === "string" && value.startsWith(SECRET_SENTINEL_PREFIX)) {
|
|
84
128
|
if (this.#secretService) {
|
|
85
129
|
const bunSecretName = value.slice(SECRET_SENTINEL_PREFIX.length);
|
|
86
130
|
await this.#secretService.deleteSecret(bunSecretName);
|
|
87
131
|
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
await this.#deleteSecrets(item);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "object" && value !== null) {
|
|
141
|
+
for (const propertyValue of Object.values(value)) {
|
|
142
|
+
await this.#deleteSecrets(propertyValue as ValueNode);
|
|
143
|
+
}
|
|
88
144
|
}
|
|
89
|
-
this.#keyValueData.delete(key);
|
|
90
|
-
this.#dirty = true;
|
|
91
145
|
}
|
|
92
146
|
}
|