@flowscripter/dynamic-cli-framework 12.1.0 → 12.2.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 (49) hide show
  1. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  2. package/dist/src/cli/BaseCLI.js +1 -1
  3. package/dist/src/runtime/runner.d.ts.map +1 -1
  4. package/dist/src/runtime/runner.js +4 -1
  5. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +23 -13
  6. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  7. package/dist/src/service/configuration/ConfigurationServiceProvider.js +69 -179
  8. package/dist/src/service/configuration/DefaultConfigurationService.d.ts +78 -0
  9. package/dist/src/service/configuration/DefaultConfigurationService.d.ts.map +1 -0
  10. package/dist/src/service/configuration/DefaultConfigurationService.js +296 -0
  11. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  12. package/dist/src/service/configuration/DefaultKeyValueService.js +7 -3
  13. package/dist/src/service/configuration/DefaultSecretService.d.ts +17 -2
  14. package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
  15. package/dist/src/service/configuration/DefaultSecretService.js +44 -5
  16. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +3 -1
  17. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  18. package/dist/src/service/configuration/KeyValueServiceProvider.js +6 -3
  19. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts +3 -7
  20. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts.map +1 -1
  21. package/dist/src/service/configuration/command/ConfigDumpSubCommand.js +5 -10
  22. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts +16 -0
  23. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts.map +1 -0
  24. package/dist/src/service/configuration/command/ConfigGetSubCommand.js +137 -0
  25. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts +18 -0
  26. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts.map +1 -0
  27. package/dist/src/service/configuration/command/ConfigSetSubCommand.js +289 -0
  28. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts +15 -0
  29. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts.map +1 -0
  30. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.js +101 -0
  31. package/dist/src/service/configuration/configTargets.d.ts +91 -0
  32. package/dist/src/service/configuration/configTargets.d.ts.map +1 -0
  33. package/dist/src/service/configuration/configTargets.js +348 -0
  34. package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -1
  35. package/dist/src/service/configuration/resolveSecrets.js +7 -1
  36. package/package.json +1 -1
  37. package/src/cli/BaseCLI.ts +1 -0
  38. package/src/runtime/runner.ts +4 -1
  39. package/src/service/configuration/ConfigurationServiceProvider.ts +95 -227
  40. package/src/service/configuration/DefaultConfigurationService.ts +378 -0
  41. package/src/service/configuration/DefaultKeyValueService.ts +9 -3
  42. package/src/service/configuration/DefaultSecretService.ts +51 -8
  43. package/src/service/configuration/KeyValueServiceProvider.ts +7 -2
  44. package/src/service/configuration/command/ConfigDumpSubCommand.ts +6 -10
  45. package/src/service/configuration/command/ConfigGetSubCommand.ts +198 -0
  46. package/src/service/configuration/command/ConfigSetSubCommand.ts +401 -0
  47. package/src/service/configuration/command/ConfigUnsetSubCommand.ts +126 -0
  48. package/src/service/configuration/configTargets.ts +481 -0
  49. package/src/service/configuration/resolveSecrets.ts +7 -1
@@ -0,0 +1,348 @@
1
+ import { MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH, SECRET_SENTINEL_PREFIX, ValueTypeName, } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { isComplexOption } from "../../runtime/argument/ArgumentTypeGuards.js";
3
+ import { getInvalidArgumentString, runCustomValidation, validateGlobalCommandArgumentValue, validateOptionValue, } from "../../runtime/values/argumentValueValidation.js";
4
+ import { getGlobalCommandValueFromEnvVars, getSubCommandValuesFromEnvVars, } from "../../util/envVarHelper.js";
5
+ import { isGlobalModifierCommand } from "../../runtime/command/CommandTypeGuards.js";
6
+ export const MASK = "********";
7
+ /**
8
+ * Collect all commands in the registry with `enableConfiguration` set, except the `config` modifier.
9
+ */
10
+ export function getConfigTargets(registry) {
11
+ const targets = new Map();
12
+ const globals = [...registry.getGlobalCommands(), ...registry.getGlobalModifierCommands()];
13
+ for (const command of globals) {
14
+ if (command.enableConfiguration !== true || command.argument === undefined) {
15
+ continue;
16
+ }
17
+ if (isGlobalModifierCommand(command) && command.name === "config") {
18
+ continue;
19
+ }
20
+ targets.set(command.name, {
21
+ key: command.name,
22
+ command,
23
+ argument: command.argument,
24
+ options: [],
25
+ });
26
+ }
27
+ for (const command of registry.getSubCommands()) {
28
+ if (command.enableConfiguration === true) {
29
+ targets.set(command.name, { key: command.name, command, options: command.options });
30
+ }
31
+ }
32
+ for (const [key, { groupCommand, command }] of registry.getGroupAndMemberCommandsByJoinedName()) {
33
+ if (command.enableConfiguration === true) {
34
+ targets.set(key, { key, command, group: groupCommand, options: command.options });
35
+ }
36
+ }
37
+ return targets;
38
+ }
39
+ /**
40
+ * Parse an option path such as `address[1].port` into steps.
41
+ */
42
+ export function parseOptionPath(path) {
43
+ const elements = path.split(".");
44
+ if (elements.length > MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH) {
45
+ return `option path '${path}' exceeds the maximum nesting depth`;
46
+ }
47
+ const steps = [];
48
+ for (const element of elements) {
49
+ const match = /^([^[\]]+)(?:\[(\d+)\])?$/.exec(element);
50
+ if (match === null) {
51
+ return `invalid option path '${path}'`;
52
+ }
53
+ steps.push(match[2] === undefined ? { name: match[1] } : { name: match[1], index: Number(match[2]) });
54
+ }
55
+ return steps;
56
+ }
57
+ /**
58
+ * Walk the steps through the target's options returning the option the path refers to.
59
+ */
60
+ export function resolveOptionPath(target, path) {
61
+ const steps = parseOptionPath(path);
62
+ if (typeof steps === "string") {
63
+ return steps;
64
+ }
65
+ let candidates = target.options;
66
+ let leaf;
67
+ for (let i = 0; i < steps.length; i++) {
68
+ const step = steps[i];
69
+ leaf = candidates.find((o) => o.name === step.name);
70
+ if (leaf === undefined) {
71
+ return `unknown option '${step.name}' in '${path}'`;
72
+ }
73
+ if (step.index !== undefined && leaf.isArray !== true) {
74
+ return `option '${step.name}' is not an array`;
75
+ }
76
+ if (i < steps.length - 1) {
77
+ if (!isComplexOption(leaf)) {
78
+ return `option '${step.name}' has no properties`;
79
+ }
80
+ if (leaf.isArray === true && step.index === undefined) {
81
+ return `option '${step.name}' is an array and requires an index`;
82
+ }
83
+ candidates = leaf.properties;
84
+ }
85
+ }
86
+ return { steps, leaf: leaf };
87
+ }
88
+ export function containsSecret(option) {
89
+ if (isComplexOption(option)) {
90
+ return option.properties.some(containsSecret);
91
+ }
92
+ return option.type === ValueTypeName.SECRET;
93
+ }
94
+ function asOptional(option) {
95
+ if (isComplexOption(option)) {
96
+ return {
97
+ ...option,
98
+ isOptional: true,
99
+ properties: option.properties.map(asOptional),
100
+ };
101
+ }
102
+ return { ...option, isOptional: true };
103
+ }
104
+ function clone(value) {
105
+ return value === undefined ? value : structuredClone(value);
106
+ }
107
+ export function getAtPath(root, steps) {
108
+ let current = root;
109
+ for (const step of steps) {
110
+ if (typeof current !== "object" || current === null) {
111
+ return undefined;
112
+ }
113
+ current = current[step.name];
114
+ if (step.index !== undefined) {
115
+ if (!Array.isArray(current)) {
116
+ return undefined;
117
+ }
118
+ current = current[step.index];
119
+ }
120
+ }
121
+ return current;
122
+ }
123
+ /**
124
+ * Set a value at the path in the provided (mutable) root creating parents as needed.
125
+ */
126
+ export function setAtPath(root, steps, value) {
127
+ let container = root;
128
+ steps.forEach((step, i) => {
129
+ const isLast = i === steps.length - 1;
130
+ if (step.index === undefined) {
131
+ if (isLast) {
132
+ container[step.name] = value;
133
+ return;
134
+ }
135
+ if (typeof container[step.name] !== "object" || container[step.name] === null) {
136
+ container[step.name] = {};
137
+ }
138
+ container = container[step.name];
139
+ return;
140
+ }
141
+ if (!Array.isArray(container[step.name])) {
142
+ container[step.name] = [];
143
+ }
144
+ const array = container[step.name];
145
+ if (step.index > array.length) {
146
+ throw new Error(`cannot set '${step.name}[${step.index}]': array has ${array.length} element(s) and sparse arrays are not supported`);
147
+ }
148
+ if (isLast) {
149
+ array[step.index] = value;
150
+ return;
151
+ }
152
+ if (typeof array[step.index] !== "object" || array[step.index] === null) {
153
+ array[step.index] = {};
154
+ }
155
+ container = array[step.index];
156
+ });
157
+ }
158
+ /**
159
+ * Remove the value at the path and any parent objects left empty. Returns false if there was no value.
160
+ */
161
+ export function removeAtPath(root, steps) {
162
+ if (getAtPath(root, steps) === undefined) {
163
+ return false;
164
+ }
165
+ const last = steps[steps.length - 1];
166
+ if (last.index !== undefined) {
167
+ throw new Error("unsetting an array element is not supported: unset the whole option instead");
168
+ }
169
+ const parents = [root];
170
+ let container = root;
171
+ for (const step of steps.slice(0, -1)) {
172
+ container =
173
+ step.index === undefined
174
+ ? container[step.name]
175
+ : container[step.name][step.index];
176
+ parents.push(container);
177
+ }
178
+ delete container[last.name];
179
+ for (let i = steps.length - 2; i >= 0; i--) {
180
+ const step = steps[i];
181
+ if (step.index !== undefined || Object.keys(parents[i + 1]).length > 0) {
182
+ break;
183
+ }
184
+ delete parents[i][step.name];
185
+ }
186
+ return true;
187
+ }
188
+ /**
189
+ * All `__SECRET__:` sentinel strings within the value.
190
+ */
191
+ export function collectSentinels(value) {
192
+ if (typeof value === "string") {
193
+ return value.startsWith(SECRET_SENTINEL_PREFIX) ? [value] : [];
194
+ }
195
+ if (Array.isArray(value)) {
196
+ return value.flatMap(collectSentinels);
197
+ }
198
+ if (typeof value === "object" && value !== null) {
199
+ return Object.values(value).flatMap(collectSentinels);
200
+ }
201
+ return [];
202
+ }
203
+ /**
204
+ * @param secrets plain text SECRET values which must not appear in the message.
205
+ */
206
+ function invalidMessage(invalid, secrets = []) {
207
+ return invalid
208
+ .map((i) => {
209
+ const hidden = secrets.includes(i.value) ? { ...i, value: undefined } : i;
210
+ return getInvalidArgumentString(hidden, false) + (i.message ?? "");
211
+ })
212
+ .join("; ");
213
+ }
214
+ /**
215
+ * Validate and convert the raw (string) value for the option path of the target. Required checks are
216
+ * not applied. Custom validators of the target are run later by {@link validateChanges}.
217
+ */
218
+ export function prepareChange(target, path, raw, deferred) {
219
+ const resolved = resolveOptionPath(target, path);
220
+ if (typeof resolved === "string") {
221
+ return resolved;
222
+ }
223
+ const { steps, leaf } = resolved;
224
+ const hasIndex = steps[steps.length - 1].index !== undefined;
225
+ const whole = isComplexOption(leaf) || (leaf.isArray === true && !hasIndex);
226
+ const invalid = [];
227
+ let value;
228
+ if (whole) {
229
+ if (!/^\s*[[{]/.test(raw)) {
230
+ return `option '${path}' requires a JSON ${leaf.isArray === true && !hasIndex ? "array" : "object"} value`;
231
+ }
232
+ if (containsSecret(leaf)) {
233
+ return `option '${path}' contains SECRET values: set each secret property individually`;
234
+ }
235
+ try {
236
+ value = JSON.parse(raw);
237
+ }
238
+ catch (err) {
239
+ return `invalid JSON value for '${path}': ${err.message}`;
240
+ }
241
+ value = validateOptionValue(asOptional(leaf), value, invalid, deferred);
242
+ }
243
+ else {
244
+ value = validateOptionValue({ ...asOptional(leaf), isArray: false }, raw, invalid, deferred);
245
+ }
246
+ if (invalid.length > 0) {
247
+ return `${path}: ${invalidMessage(invalid, [raw])}`;
248
+ }
249
+ const isSecret = !isComplexOption(leaf) && leaf.type === ValueTypeName.SECRET;
250
+ return { steps, path, leaf, value, isSecret };
251
+ }
252
+ /**
253
+ * Validate a raw value for a global command argument.
254
+ */
255
+ export function prepareGlobalValue(target, raw, deferred) {
256
+ const invalid = [];
257
+ const argument = { ...target.argument, isOptional: true };
258
+ const value = validateGlobalCommandArgumentValue({ ...target.command, argument }, raw, invalid, deferred);
259
+ if (invalid.length > 0 || value === undefined) {
260
+ return invalidMessage(invalid, [raw]);
261
+ }
262
+ return { value, isSecret: target.argument.type === ValueTypeName.SECRET };
263
+ }
264
+ /**
265
+ * Apply the changes to a copy of the existing values and run the custom validators of the target.
266
+ * Returns the new values or an error message.
267
+ */
268
+ export function applyChanges(target, existing, changes, deferred) {
269
+ const values = clone(existing ?? {});
270
+ try {
271
+ for (const change of changes) {
272
+ setAtPath(values, change.steps, change.value);
273
+ }
274
+ }
275
+ catch (err) {
276
+ return err.message;
277
+ }
278
+ const invalid = [];
279
+ runCustomValidation(deferred, target.command, values, invalid);
280
+ if (invalid.length > 0) {
281
+ return invalidMessage(invalid, changes.filter((c) => c.isSecret).map((c) => c.value));
282
+ }
283
+ return values;
284
+ }
285
+ function maskSentinels(value) {
286
+ if (typeof value === "string") {
287
+ return value.startsWith(SECRET_SENTINEL_PREFIX) ? MASK : value;
288
+ }
289
+ if (Array.isArray(value)) {
290
+ return value.map(maskSentinels);
291
+ }
292
+ if (typeof value === "object" && value !== null) {
293
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskSentinels(v)]));
294
+ }
295
+ return value;
296
+ }
297
+ function maskOption(option, value) {
298
+ if (value === undefined || value === null) {
299
+ return value;
300
+ }
301
+ if (isComplexOption(option)) {
302
+ if (Array.isArray(value)) {
303
+ return value.map((v) => maskOption({ ...option, isArray: false }, v));
304
+ }
305
+ return maskOptions(option.properties, value);
306
+ }
307
+ if (option.type === ValueTypeName.SECRET) {
308
+ return Array.isArray(value) ? value.map(() => MASK) : MASK;
309
+ }
310
+ return value;
311
+ }
312
+ function maskOptions(options, value) {
313
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
314
+ return value;
315
+ }
316
+ return Object.fromEntries(Object.entries(value).map(([name, v]) => {
317
+ const option = options.find((o) => o.name === name);
318
+ return [name, option === undefined ? v : maskOption(option, v)];
319
+ }));
320
+ }
321
+ /**
322
+ * Copy of a stored default value with SECRET typed values and any secret sentinels masked.
323
+ */
324
+ export function maskDefault(target, value) {
325
+ let masked = value;
326
+ if (target?.argument !== undefined) {
327
+ if (target.argument.type === ValueTypeName.SECRET && value !== undefined) {
328
+ masked = MASK;
329
+ }
330
+ }
331
+ else if (target !== undefined) {
332
+ masked = maskOptions(target.options, value);
333
+ }
334
+ return maskSentinels(masked);
335
+ }
336
+ /**
337
+ * Whether an environment variable would override the stored value at the path.
338
+ */
339
+ export function isOverriddenByEnvVar(provider, cliConfig, target, steps) {
340
+ if (!provider.envVarsEnabled) {
341
+ return false;
342
+ }
343
+ if (target.argument !== undefined) {
344
+ return (getGlobalCommandValueFromEnvVars(cliConfig, target.command) !== undefined);
345
+ }
346
+ const values = getSubCommandValuesFromEnvVars(cliConfig, target.command, target.group);
347
+ return getAtPath(values, steps) !== undefined;
348
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"resolveSecrets.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/resolveSecrets.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAA8B,cAAc,CAAC,CAAC,EAC5C,KAAK,EAAE,CAAC,EACR,aAAa,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GACzD,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
1
+ {"version":3,"file":"resolveSecrets.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/resolveSecrets.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAA8B,cAAc,CAAC,CAAC,EAC5C,KAAK,EAAE,CAAC,EACR,aAAa,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GACzD,OAAO,CAAC,CAAC,CAAC,CA0BZ"}
@@ -23,7 +23,13 @@ export default async function resolveSecrets(value, resolveSecret) {
23
23
  }
24
24
  const resolved = {};
25
25
  for (const [key, val] of Object.entries(value)) {
26
- resolved[key] = await resolveSecrets(val, resolveSecret);
26
+ // defineProperty so a "__proto__" key is an own property rather than replacing the prototype
27
+ Object.defineProperty(resolved, key, {
28
+ value: await resolveSecrets(val, resolveSecret),
29
+ enumerable: true,
30
+ writable: true,
31
+ configurable: true,
32
+ });
27
33
  }
28
34
  return resolved;
29
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "12.1.0",
3
+ "version": "12.2.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",
@@ -395,6 +395,7 @@ export default class BaseCLI implements CLI {
395
395
  this.#options.configFileSupportEnabled,
396
396
  this.#options.secretServiceEnabled,
397
397
  this.#options.configFormat,
398
+ this.#commandRegistry,
398
399
  );
399
400
  this.addServiceProvider(configurationServiceProvider);
400
401
  const keyValueServiceProvider = new KeyValueServiceProvider(
@@ -121,8 +121,11 @@ async function executeParsedCommand(
121
121
  parseResult!.populatedArgumentValues,
122
122
  );
123
123
 
124
+ const commandScopeKey = parseResult.groupCommand
125
+ ? `${parseResult.groupCommand.name}:${parseResult.command.name}`
126
+ : parseResult.command.name;
124
127
  const commandContext = keyValueServiceProvider
125
- ? keyValueServiceProvider.getContextForScope(context, "command", parseResult.command.name)
128
+ ? keyValueServiceProvider.getContextForScope(context, "command", commandScopeKey)
126
129
  : context;
127
130
  if (isSubCommand(parseResult.command)) {
128
131
  await parseResult.command.execute(