@flowscripter/dynamic-cli-framework 12.0.1 → 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 (80) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  5. package/dist/src/cli/BaseCLI.js +3 -1
  6. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +6 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  8. package/dist/src/runtime/runner.d.ts.map +1 -1
  9. package/dist/src/runtime/runner.js +4 -1
  10. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +33 -15
  11. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  12. package/dist/src/service/configuration/ConfigurationServiceProvider.js +80 -173
  13. package/dist/src/service/configuration/DefaultConfigurationService.d.ts +78 -0
  14. package/dist/src/service/configuration/DefaultConfigurationService.d.ts.map +1 -0
  15. package/dist/src/service/configuration/DefaultConfigurationService.js +296 -0
  16. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  17. package/dist/src/service/configuration/DefaultKeyValueService.js +7 -3
  18. package/dist/src/service/configuration/DefaultSecretService.d.ts +17 -2
  19. package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
  20. package/dist/src/service/configuration/DefaultSecretService.js +44 -5
  21. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +3 -1
  22. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  23. package/dist/src/service/configuration/KeyValueServiceProvider.js +6 -3
  24. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts +3 -7
  25. package/dist/src/service/configuration/command/ConfigDumpSubCommand.d.ts.map +1 -1
  26. package/dist/src/service/configuration/command/ConfigDumpSubCommand.js +5 -10
  27. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts +16 -0
  28. package/dist/src/service/configuration/command/ConfigGetSubCommand.d.ts.map +1 -0
  29. package/dist/src/service/configuration/command/ConfigGetSubCommand.js +137 -0
  30. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts +18 -0
  31. package/dist/src/service/configuration/command/ConfigSetSubCommand.d.ts.map +1 -0
  32. package/dist/src/service/configuration/command/ConfigSetSubCommand.js +289 -0
  33. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts +15 -0
  34. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.d.ts.map +1 -0
  35. package/dist/src/service/configuration/command/ConfigUnsetSubCommand.js +101 -0
  36. package/dist/src/service/configuration/configTargets.d.ts +91 -0
  37. package/dist/src/service/configuration/configTargets.d.ts.map +1 -0
  38. package/dist/src/service/configuration/configTargets.js +348 -0
  39. package/dist/src/service/configuration/format/ConfigFormat.d.ts +42 -0
  40. package/dist/src/service/configuration/format/ConfigFormat.d.ts.map +1 -0
  41. package/dist/src/service/configuration/format/ConfigFormat.js +0 -0
  42. package/dist/src/service/configuration/format/JsonConfigFormat.d.ts +7 -0
  43. package/dist/src/service/configuration/format/JsonConfigFormat.d.ts.map +1 -0
  44. package/dist/src/service/configuration/format/JsonConfigFormat.js +17 -0
  45. package/dist/src/service/configuration/format/TomlConfigFormat.d.ts +10 -0
  46. package/dist/src/service/configuration/format/TomlConfigFormat.d.ts.map +1 -0
  47. package/dist/src/service/configuration/format/TomlConfigFormat.js +78 -0
  48. package/dist/src/service/configuration/format/YamlConfigFormat.d.ts +9 -0
  49. package/dist/src/service/configuration/format/YamlConfigFormat.d.ts.map +1 -0
  50. package/dist/src/service/configuration/format/YamlConfigFormat.js +63 -0
  51. package/dist/src/service/configuration/resolveSecrets.d.ts.map +1 -1
  52. package/dist/src/service/configuration/resolveSecrets.js +7 -1
  53. package/dist/toml.d.ts +2 -0
  54. package/dist/toml.d.ts.map +1 -0
  55. package/dist/toml.js +1 -0
  56. package/dist/yaml.d.ts +2 -0
  57. package/dist/yaml.d.ts.map +1 -0
  58. package/dist/yaml.js +1 -0
  59. package/index.ts +3 -0
  60. package/package.json +27 -3
  61. package/src/cli/BaseCLI.ts +4 -0
  62. package/src/cli/BaseCLIFeatureOptions.ts +6 -0
  63. package/src/runtime/runner.ts +4 -1
  64. package/src/service/configuration/ConfigurationServiceProvider.ts +108 -214
  65. package/src/service/configuration/DefaultConfigurationService.ts +378 -0
  66. package/src/service/configuration/DefaultKeyValueService.ts +9 -3
  67. package/src/service/configuration/DefaultSecretService.ts +51 -8
  68. package/src/service/configuration/KeyValueServiceProvider.ts +7 -2
  69. package/src/service/configuration/command/ConfigDumpSubCommand.ts +6 -10
  70. package/src/service/configuration/command/ConfigGetSubCommand.ts +198 -0
  71. package/src/service/configuration/command/ConfigSetSubCommand.ts +401 -0
  72. package/src/service/configuration/command/ConfigUnsetSubCommand.ts +126 -0
  73. package/src/service/configuration/configTargets.ts +481 -0
  74. package/src/service/configuration/format/ConfigFormat.ts +42 -0
  75. package/src/service/configuration/format/JsonConfigFormat.ts +22 -0
  76. package/src/service/configuration/format/TomlConfigFormat.ts +92 -0
  77. package/src/service/configuration/format/YamlConfigFormat.ts +72 -0
  78. package/src/service/configuration/resolveSecrets.ts +7 -1
  79. package/toml.ts +1 -0
  80. package/yaml.ts +1 -0
@@ -0,0 +1,91 @@
1
+ import { type Command, type ComplexOption, type GlobalCommandArgument, type GroupCommand, type Option, type CommandRegistry, type CLIConfig, type SingleValueType, type Values, type ValueNode } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { type DeferredCustomValidation } from "../../runtime/values/argumentValueValidation.ts";
3
+ export declare const MASK = "********";
4
+ type Node = unknown;
5
+ /**
6
+ * A command whose default argument values can be managed via the `config` group commands.
7
+ */
8
+ export interface ConfigTarget {
9
+ /** The key under `defaults`: the command name or `<group name>:<member name>`. */
10
+ readonly key: string;
11
+ readonly command: Command;
12
+ readonly group?: GroupCommand;
13
+ /** Set for global commands and global modifier commands. */
14
+ readonly argument?: GlobalCommandArgument;
15
+ /** Empty for global commands. */
16
+ readonly options: ReadonlyArray<Option | ComplexOption>;
17
+ }
18
+ /**
19
+ * Collect all commands in the registry with `enableConfiguration` set, except the `config` modifier.
20
+ */
21
+ export declare function getConfigTargets(registry: CommandRegistry): Map<string, ConfigTarget>;
22
+ export interface PathStep {
23
+ readonly name: string;
24
+ readonly index?: number;
25
+ }
26
+ /**
27
+ * Parse an option path such as `address[1].port` into steps.
28
+ */
29
+ export declare function parseOptionPath(path: string): Array<PathStep> | string;
30
+ export interface ResolvedPath {
31
+ readonly steps: Array<PathStep>;
32
+ readonly leaf: Option | ComplexOption;
33
+ }
34
+ /**
35
+ * Walk the steps through the target's options returning the option the path refers to.
36
+ */
37
+ export declare function resolveOptionPath(target: ConfigTarget, path: string): ResolvedPath | string;
38
+ export declare function containsSecret(option: Option | ComplexOption): boolean;
39
+ export declare function getAtPath(root: Node, steps: ReadonlyArray<PathStep>): Node;
40
+ /**
41
+ * Set a value at the path in the provided (mutable) root creating parents as needed.
42
+ */
43
+ export declare function setAtPath(root: Record<string, Node>, steps: ReadonlyArray<PathStep>, value: Node): void;
44
+ /**
45
+ * Remove the value at the path and any parent objects left empty. Returns false if there was no value.
46
+ */
47
+ export declare function removeAtPath(root: Record<string, Node>, steps: ReadonlyArray<PathStep>): boolean;
48
+ /**
49
+ * All `__SECRET__:` sentinel strings within the value.
50
+ */
51
+ export declare function collectSentinels(value: Node): Array<string>;
52
+ /**
53
+ * A validated, converted change to a single option of a target.
54
+ */
55
+ export interface ConfigChange {
56
+ readonly steps: ReadonlyArray<PathStep>;
57
+ readonly path: string;
58
+ readonly leaf: Option | ComplexOption;
59
+ /** Native value, or the plain text of a SECRET which must be stored via the secret service. */
60
+ readonly value: Node;
61
+ readonly isSecret: boolean;
62
+ }
63
+ /**
64
+ * Validate and convert the raw (string) value for the option path of the target. Required checks are
65
+ * not applied. Custom validators of the target are run later by {@link validateChanges}.
66
+ */
67
+ export declare function prepareChange(target: ConfigTarget, path: string, raw: string, deferred: Array<DeferredCustomValidation>): ConfigChange | string;
68
+ /**
69
+ * Validate a raw value for a global command argument.
70
+ */
71
+ export declare function prepareGlobalValue(target: ConfigTarget, raw: string, deferred: Array<DeferredCustomValidation>): {
72
+ value: SingleValueType;
73
+ isSecret: boolean;
74
+ } | string;
75
+ /**
76
+ * Apply the changes to a copy of the existing values and run the custom validators of the target.
77
+ * Returns the new values or an error message.
78
+ */
79
+ export declare function applyChanges(target: ConfigTarget, existing: Values | undefined, changes: ReadonlyArray<ConfigChange>, deferred: Array<DeferredCustomValidation>): Values | string;
80
+ /**
81
+ * Copy of a stored default value with SECRET typed values and any secret sentinels masked.
82
+ */
83
+ export declare function maskDefault(target: ConfigTarget | undefined, value: Node): Node;
84
+ export type { ValueNode };
85
+ /**
86
+ * Whether an environment variable would override the stored value at the path.
87
+ */
88
+ export declare function isOverriddenByEnvVar(provider: {
89
+ readonly envVarsEnabled: boolean;
90
+ }, cliConfig: CLIConfig, target: ConfigTarget, steps: ReadonlyArray<PathStep>): boolean;
91
+ //# sourceMappingURL=configTargets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configTargets.d.ts","sourceRoot":"","sources":["../../../../src/service/configuration/configTargets.ts"],"names":[],"mappings":"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"}
@@ -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
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The top level structure of a configuration file: `defaults` and `key-values` objects.
3
+ */
4
+ export type ConfigData = Record<string, any>;
5
+ /**
6
+ * Defines the on-disk syntax of the CLI configuration file.
7
+ *
8
+ * The built-in implementation is JSON (the default). YAML and TOML implementations are available
9
+ * from the `@flowscripter/dynamic-cli-framework/yaml` and `@flowscripter/dynamic-cli-framework/toml`
10
+ * entry points. Third party formats can be supplied by implementing this interface.
11
+ */
12
+ export default interface ConfigFormat {
13
+ /**
14
+ * The extension (without the leading dot) used for the default configuration file name,
15
+ * e.g. `json`.
16
+ */
17
+ readonly fileExtension: string;
18
+ /**
19
+ * Parse configuration file text.
20
+ *
21
+ * @returns the parsed `data` and an opaque `handle` retaining whatever the format needs to write
22
+ * changes back while preserving comments and layout (`undefined` if the format has nothing to retain).
23
+ * @throws if the text is not valid for this format.
24
+ */
25
+ parse(text: string): {
26
+ data: ConfigData;
27
+ handle: unknown;
28
+ };
29
+ /**
30
+ * Produce updated file text from a `handle` returned by {@link parse}.
31
+ *
32
+ * @param handle the handle returned by {@link parse} for the text being updated.
33
+ * @param changes the complete updated configuration data. The format reconciles the existing text
34
+ * with it, leaving unchanged content (including comments) untouched.
35
+ */
36
+ apply(handle: unknown, changes: ConfigData): string;
37
+ /**
38
+ * Produce new file text from scratch.
39
+ */
40
+ serialize(data: ConfigData): string;
41
+ }
42
+ //# sourceMappingURL=ConfigFormat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigFormat.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/format/ConfigFormat.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,WAAW,YAAY;IACnC;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAE3D;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC;IAEpD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;CACrC"}
@@ -0,0 +1,7 @@
1
+ import type ConfigFormat from "./ConfigFormat.ts";
2
+ /**
3
+ * The built-in JSON {@link ConfigFormat}. The file is regenerated on write: JSON has no comments to preserve.
4
+ */
5
+ declare const jsonFormat: ConfigFormat;
6
+ export default jsonFormat;
7
+ //# sourceMappingURL=JsonConfigFormat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonConfigFormat.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/format/JsonConfigFormat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAC;AAOlD;;GAEG;AACH,QAAA,MAAM,UAAU,EAAE,YASjB,CAAC;eAEa,UAAU"}
@@ -0,0 +1,17 @@
1
+ function serialize(data) {
2
+ return JSON.stringify(data, null, 2);
3
+ }
4
+ /**
5
+ * The built-in JSON {@link ConfigFormat}. The file is regenerated on write: JSON has no comments to preserve.
6
+ */
7
+ const jsonFormat = {
8
+ fileExtension: "json",
9
+ parse(text) {
10
+ return { data: JSON.parse(text), handle: undefined };
11
+ },
12
+ apply(_handle, changes) {
13
+ return serialize(changes);
14
+ },
15
+ serialize,
16
+ };
17
+ export default jsonFormat;
@@ -0,0 +1,10 @@
1
+ import type ConfigFormat from "./ConfigFormat.ts";
2
+ /**
3
+ * A TOML {@link ConfigFormat} which preserves comments, whitespace and layout when writing changes back.
4
+ * Dates and times are not supported as configuration values.
5
+ *
6
+ * Requires the optional `@decimalturn/toml-patch` peer dependency.
7
+ */
8
+ declare const tomlFormat: ConfigFormat;
9
+ export default tomlFormat;
10
+ //# sourceMappingURL=TomlConfigFormat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TomlConfigFormat.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/format/TomlConfigFormat.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAC;AAoElD;;;;;GAKG;AACH,QAAA,MAAM,UAAU,EAAE,YAajB,CAAC;eAEa,UAAU"}
@@ -0,0 +1,78 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { parse, patch, stringify } from "@decimalturn/toml-patch";
3
+ // new tables nested deeper than this (e.g. `defaults.<command>.<option>`) are left to patch() to
4
+ // lay out as inline tables
5
+ const MAX_TABLE_HEADER_DEPTH = 3;
6
+ function isPlainObject(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function formatKey(key) {
10
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
11
+ }
12
+ function collectNewTables(existing, updated, path, newTables) {
13
+ for (const [key, value] of Object.entries(updated)) {
14
+ if (!isPlainObject(value) || path.length + 1 > MAX_TABLE_HEADER_DEPTH) {
15
+ continue;
16
+ }
17
+ const childPath = [...path, key];
18
+ const existingChild = isPlainObject(existing) ? existing[key] : undefined;
19
+ if (existingChild === undefined) {
20
+ newTables.push(childPath);
21
+ }
22
+ collectNewTables(existingChild, value, childPath, newTables);
23
+ }
24
+ }
25
+ // parse() results do not share prototypes with plain objects
26
+ function plain(value) {
27
+ return JSON.parse(JSON.stringify(value));
28
+ }
29
+ /**
30
+ * Append an empty `[table]` header for every new table, so that patch() fills it in at the end
31
+ * of the file rather than hoisting it to the top as a dotted key. Returns the original text if the
32
+ * result is not equivalent to it plus the empty tables (e.g. the parent is an inline table which
33
+ * cannot be extended).
34
+ */
35
+ function addNewTableHeaders(text, existing, changes) {
36
+ const newTables = [];
37
+ collectNewTables(existing, changes, [], newTables);
38
+ if (newTables.length === 0) {
39
+ return text;
40
+ }
41
+ const expected = structuredClone(existing);
42
+ for (const tablePath of newTables) {
43
+ let node = expected;
44
+ for (const key of tablePath.slice(0, -1)) {
45
+ node = (node[key] ??= {});
46
+ }
47
+ node[tablePath[tablePath.length - 1]] ??= {};
48
+ }
49
+ const headers = newTables.map((tablePath) => `[${tablePath.map(formatKey).join(".")}]`);
50
+ const withHeaders = `${text.trimEnd()}\n\n${headers.join("\n\n")}\n`;
51
+ try {
52
+ return isDeepStrictEqual(plain(parse(withHeaders)), plain(expected)) ? withHeaders : text;
53
+ }
54
+ catch {
55
+ return text;
56
+ }
57
+ }
58
+ /**
59
+ * A TOML {@link ConfigFormat} which preserves comments, whitespace and layout when writing changes back.
60
+ * Dates and times are not supported as configuration values.
61
+ *
62
+ * Requires the optional `@decimalturn/toml-patch` peer dependency.
63
+ */
64
+ const tomlFormat = {
65
+ fileExtension: "toml",
66
+ parse(text) {
67
+ return { data: parse(text), handle: text };
68
+ },
69
+ apply(handle, changes) {
70
+ const text = handle;
71
+ const existing = parse(text);
72
+ return patch(addNewTableHeaders(text, existing, changes), changes);
73
+ },
74
+ serialize(data) {
75
+ return stringify(data);
76
+ },
77
+ };
78
+ export default tomlFormat;
@@ -0,0 +1,9 @@
1
+ import type ConfigFormat from "./ConfigFormat.ts";
2
+ /**
3
+ * A YAML {@link ConfigFormat} which preserves comments, blank lines and key order when writing changes back.
4
+ *
5
+ * Requires the optional `yaml` peer dependency.
6
+ */
7
+ declare const yamlFormat: ConfigFormat;
8
+ export default yamlFormat;
9
+ //# sourceMappingURL=YamlConfigFormat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YamlConfigFormat.d.ts","sourceRoot":"","sources":["../../../../../src/service/configuration/format/YamlConfigFormat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAC;AAqClD;;;;GAIG;AACH,QAAA,MAAM,UAAU,EAAE,YA0BjB,CAAC;eAEa,UAAU"}