@flowscripter/dynamic-cli-framework 9.1.3 → 10.1.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 (47) hide show
  1. package/dist/src/command/MultiCommandCliHelpCommand.d.ts.map +1 -1
  2. package/dist/src/command/MultiCommandCliHelpCommand.js +1 -1
  3. package/dist/src/runtime/command/CommandValidator.d.ts.map +1 -1
  4. package/dist/src/runtime/command/CommandValidator.js +9 -2
  5. package/dist/src/runtime/parser.d.ts +8 -2
  6. package/dist/src/runtime/parser.d.ts.map +1 -1
  7. package/dist/src/runtime/parser.js +11 -6
  8. package/dist/src/runtime/runner.d.ts.map +1 -1
  9. package/dist/src/runtime/runner.js +5 -2
  10. package/dist/src/runtime/values/ValuePopulationResult.d.ts +4 -0
  11. package/dist/src/runtime/values/ValuePopulationResult.d.ts.map +1 -1
  12. package/dist/src/runtime/values/globalCommandValuePopulation.d.ts.map +1 -1
  13. package/dist/src/runtime/values/globalCommandValuePopulation.js +11 -3
  14. package/dist/src/runtime/values/subCommandValuePopulation.d.ts.map +1 -1
  15. package/dist/src/runtime/values/subCommandValuePopulation.js +39 -9
  16. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +12 -3
  17. package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
  18. package/dist/src/service/configuration/ConfigurationServiceProvider.js +16 -7
  19. package/dist/src/util/configHelper.d.ts +4 -1
  20. package/dist/src/util/configHelper.d.ts.map +1 -1
  21. package/dist/src/util/configHelper.js +6 -1
  22. package/dist/src/util/envVarHelper.d.ts +4 -1
  23. package/dist/src/util/envVarHelper.d.ts.map +1 -1
  24. package/dist/src/util/envVarHelper.js +8 -5
  25. package/dist/src/util/helpHelper.d.ts +1 -1
  26. package/dist/src/util/helpHelper.d.ts.map +1 -1
  27. package/dist/src/util/helpHelper.js +8 -16
  28. package/dist/src/util/runnerHelper.d.ts +2 -0
  29. package/dist/src/util/runnerHelper.d.ts.map +1 -1
  30. package/dist/src/util/runnerHelper.js +10 -0
  31. package/dist/src/util/secretRedaction.d.ts +11 -0
  32. package/dist/src/util/secretRedaction.d.ts.map +1 -0
  33. package/dist/src/util/secretRedaction.js +45 -0
  34. package/package.json +1 -1
  35. package/src/command/MultiCommandCliHelpCommand.ts +7 -1
  36. package/src/runtime/command/CommandValidator.ts +8 -1
  37. package/src/runtime/parser.ts +22 -16
  38. package/src/runtime/runner.ts +5 -0
  39. package/src/runtime/values/ValuePopulationResult.ts +5 -0
  40. package/src/runtime/values/globalCommandValuePopulation.ts +12 -5
  41. package/src/runtime/values/subCommandValuePopulation.ts +47 -12
  42. package/src/service/configuration/ConfigurationServiceProvider.ts +18 -6
  43. package/src/util/configHelper.ts +7 -0
  44. package/src/util/envVarHelper.ts +19 -3
  45. package/src/util/helpHelper.ts +34 -13
  46. package/src/util/runnerHelper.ts +18 -0
  47. package/src/util/secretRedaction.ts +81 -0
@@ -13,6 +13,7 @@ import type {
13
13
  } from "@flowscripter/dynamic-cli-framework-api";
14
14
  import getLogger from "../../util/logger.ts";
15
15
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
16
+ import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
16
17
  import DumpConfigCommand from "./command/DumpConfigCommand.ts";
17
18
  import {
18
19
  CONFIGURATION_SERVICE_ID,
@@ -77,7 +78,8 @@ class ConfigurationServiceImpl implements ConfigurationService {
77
78
  *
78
79
  * A JSON file where the structure matches {@link Values} and the defaults are stored
79
80
  * under a top level `defaults` property. The second level of properties is used to refer to {@link Command.name}
80
- * and the contained values are treated as command {@link Values}. As an example:
81
+ * (or `<group name>:<member name>` for a member of a {@link GroupCommand}) and the contained values are treated as
82
+ * command {@link Values}. As an example:
81
83
  *
82
84
  * ```
83
85
  * {
@@ -94,6 +96,9 @@ class ConfigurationServiceImpl implements ConfigurationService {
94
96
  * "command2": {
95
97
  * "arg4": true
96
98
  * },
99
+ * "group1:member1": {
100
+ * "arg5": "bar"
101
+ * },
97
102
  * "globalCommand": "globalArgumentValue"
98
103
  * },
99
104
  * "key-values": {
@@ -124,13 +129,15 @@ class ConfigurationServiceImpl implements ConfigurationService {
124
129
  * Argument configuration keys are concatenated with a `_` separator.
125
130
  * Any arguments which support array values must by suffixed with `_` and an explicit array index.
126
131
  * If the root argument in the path does not use a custom {@link Argument.configurationKey} then the key path
127
- * is additionally suffixed with the {@link CLIConfig.name} and the {@link Command.name} with `_` separators.
132
+ * is additionally prefixed with the {@link CLIConfig.name}, the {@link GroupCommand.name} (if the command is a group
133
+ * member) and the {@link Command.name} with `_` separators.
128
134
  * This is best explained with examples...
129
135
  *
130
136
  * No custom configuration key examples:
131
137
  *
132
138
  * * executable: `MyCLI`, command: `globalCommand1`, global command argument => environment variable: `MYCLI_GLOBALCOMMAND1`
133
139
  * * executable: `MyCLI`, command: `command1`, simple root argument: `arg1` => environment variable: `MYCLI_COMMAND1_ARG1`
140
+ * * executable: `MyCLI`, group: `group1`, member command: `member1`, simple root argument: `arg1` => environment variable: `MYCLI_GROUP1_MEMBER1_ARG1`
134
141
  * * executable: `MyCLI`, command: `command1`, array root argument, 1st element: `arg2[0]` => environment variable: `MYCLI_COMMAND1_ARG2_0`
135
142
  * * executable: `MyCLI`, command: `command1`, argument is a digit so it is by default suffixed with `_`: `3` => environment variable: `MYCLI_COMMAND1__3`
136
143
  * * executable: `MyCLI`, command: `command1`, nested sub-argument: `arg1.arg2` => environment variable: `MYCLI_COMMAND1_ARG1_ARG2`
@@ -238,17 +245,22 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
238
245
  *
239
246
  * @param cliConfig the {@link CLIConfig} to use for retrieving configuration keys.
240
247
  * @param command the {@link Command} instance to retrieve default argument values for.
248
+ * @param groupCommand the {@link GroupCommand} if the command is a group member. The configuration file key
249
+ * is then `<group name>:<command name>` and the env var names include the group name.
241
250
  */
242
251
  public async getDefaultArgumentValues(
243
252
  cliConfig: CLIConfig,
244
253
  command: Command,
254
+ groupCommand?: GroupCommand,
245
255
  ): Promise<PopulatedValues | PopulatedValueType | undefined> {
246
256
  if (!this.configEnabled) {
247
257
  logger.debug("configuration of default values is not enabled");
248
258
  return undefined;
249
259
  }
250
260
 
251
- logger.debug("getting default argument values for command: %s", command.name);
261
+ const commandKey = groupCommand ? `${groupCommand.name}:${command.name}` : command.name;
262
+
263
+ logger.debug("getting default argument values for command: %s", commandKey);
252
264
 
253
265
  if (command.enableConfiguration === undefined || command.enableConfiguration !== true) {
254
266
  logger.debug("enableConfiguration for command: %s is not true", command.name);
@@ -258,16 +270,16 @@ export default class ConfigurationServiceProvider implements ServiceProvider {
258
270
  let result: PopulatedValues | PopulatedValueType | undefined;
259
271
 
260
272
  if (isGlobalModifierCommand(command) || isGlobalCommand(command)) {
261
- const configuredValue = this.defaultsData.get(command.name) as SingleValueType;
273
+ const configuredValue = this.defaultsData.get(commandKey) as SingleValueType;
262
274
  const envVarValue = this.envVarsEnabled
263
275
  ? getGlobalCommandValueFromEnvVars(cliConfig, command)
264
276
  : undefined;
265
277
  // default to environment variable value
266
278
  result = envVarValue !== undefined ? envVarValue : configuredValue;
267
279
  } else if (isSubCommand(command)) {
268
- const configuredValues = this.defaultsData.get(command.name) as Values;
280
+ const configuredValues = this.defaultsData.get(commandKey) as Values;
269
281
  const envVarValues = this.envVarsEnabled
270
- ? getSubCommandValuesFromEnvVars(cliConfig, command)
282
+ ? getSubCommandValuesFromEnvVars(cliConfig, command, groupCommand)
271
283
  : undefined;
272
284
  if (envVarValues === undefined) {
273
285
  result = configuredValues;
@@ -1,3 +1,4 @@
1
+ import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
1
2
  import type { Command } from "@flowscripter/dynamic-cli-framework-api";
2
3
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
3
4
  import type { Option } from "@flowscripter/dynamic-cli-framework-api";
@@ -26,11 +27,14 @@ function getKeySegment(segment: string, isCustom = false, isArray = false) {
26
27
  * @param command the {@link Command} to use for determining the default configuration key.
27
28
  * @param argumentAncestry a list of {@link SubCommandArgument} instances from the root command arguments to a leaf argument.
28
29
  * Apart from {@link ComplexOption} instances this will always only have a single entry.
30
+ * @param groupCommand the {@link GroupCommand} if the command is a member of a group, in which case the group name
31
+ * is an additional key segment.
29
32
  */
30
33
  export function getSubCommandArgumentConfigurationKey(
31
34
  cliConfig: CLIConfig,
32
35
  command: Command,
33
36
  argumentAncestry: Array<SubCommandArgument>,
37
+ groupCommand?: GroupCommand,
34
38
  ): string | undefined {
35
39
  if (command.enableConfiguration === undefined || command.enableConfiguration !== true) {
36
40
  return undefined;
@@ -43,6 +47,9 @@ export function getSubCommandArgumentConfigurationKey(
43
47
  const firstArgument = argumentAncestry[0]!;
44
48
  if (firstArgument.configurationKey === undefined) {
45
49
  keySegments.push(getKeySegment(cliConfig.name));
50
+ if (groupCommand) {
51
+ keySegments.push(getKeySegment(groupCommand.name));
52
+ }
46
53
  keySegments.push(getKeySegment(command.name));
47
54
  }
48
55
 
@@ -14,6 +14,7 @@ import { MAXIMUM_ARGUMENT_ARRAY_SIZE } from "@flowscripter/dynamic-cli-framework
14
14
  import type { Option } from "@flowscripter/dynamic-cli-framework-api";
15
15
  import type { GlobalCommandArgument } from "@flowscripter/dynamic-cli-framework-api";
16
16
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
17
+ import type { GroupCommand } from "@flowscripter/dynamic-cli-framework-api";
17
18
  import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
18
19
  import type { ComplexOption } from "@flowscripter/dynamic-cli-framework-api";
19
20
 
@@ -29,11 +30,13 @@ function getSubCommandArgumentKeyPrefix(
29
30
  cliConfig: CLIConfig,
30
31
  command: Command,
31
32
  subCommandArgument: SubCommandArgument,
33
+ groupCommand?: GroupCommand,
32
34
  ) {
33
35
  if (subCommandArgument.configurationKey) {
34
36
  return subCommandArgument.configurationKey;
35
37
  }
36
- return `${getKeySegment(cliConfig.name)}_${getKeySegment(command.name)}_${getKeySegment(
38
+ const groupSegment = groupCommand ? `${getKeySegment(groupCommand.name)}_` : "";
39
+ return `${getKeySegment(cliConfig.name)}_${groupSegment}${getKeySegment(command.name)}_${getKeySegment(
37
40
  subCommandArgument.name,
38
41
  )}`;
39
42
  }
@@ -214,16 +217,24 @@ function getOptionValuesFromEnvVars(
214
217
  * @param cliConfig the {@link CLIConfig} to use for determining the default configuration key.
215
218
  * @param command the {@link SubCommand} to use for determining the default configuration key and containing
216
219
  * {@link Positional} and {@link Option} instances to populate values for.
220
+ * @param groupCommand the {@link GroupCommand} if the command is a group member, in which case the group name
221
+ * is an additional env var name segment.
217
222
  */
218
223
  export function getSubCommandValuesFromEnvVars(
219
224
  cliConfig: CLIConfig,
220
225
  command: SubCommand,
226
+ groupCommand?: GroupCommand,
221
227
  ): PopulatedValues | undefined {
222
228
  const envVarValues = Object.create(null) as PopulatedValues;
223
229
  if (command.options) {
224
230
  for (const option of command.options) {
225
231
  const potentialEnvVarNames = Object.keys(process.env);
226
- const optionPrefix = getSubCommandArgumentKeyPrefix(cliConfig, command, option as Option);
232
+ const optionPrefix = getSubCommandArgumentKeyPrefix(
233
+ cliConfig,
234
+ command,
235
+ option as Option,
236
+ groupCommand,
237
+ );
227
238
  getOptionValuesFromEnvVars(
228
239
  envVarValues,
229
240
  potentialEnvVarNames,
@@ -244,7 +255,12 @@ export function getSubCommandValuesFromEnvVars(
244
255
  `Unsafe key name in use: ${positional.name}, CodeQL Rule ID: js/prototype-polluting-assignment`,
245
256
  );
246
257
  }
247
- let envVarNamePrefix = getSubCommandArgumentKeyPrefix(cliConfig, command, positional);
258
+ let envVarNamePrefix = getSubCommandArgumentKeyPrefix(
259
+ cliConfig,
260
+ command,
261
+ positional,
262
+ groupCommand,
263
+ );
248
264
 
249
265
  if (positional.isVarargMultiple) {
250
266
  envVarNamePrefix = `${envVarNamePrefix}_`;
@@ -312,6 +312,7 @@ function getOptionHelpEntry(
312
312
  option: Option | ComplexOption,
313
313
  subEntryIndent = 0,
314
314
  optionAncestry: Array<Option | ComplexOption> = [],
315
+ groupCommand?: GroupCommand,
315
316
  ): HelpEntry {
316
317
  let subEntries: Array<HelpEntry> | undefined;
317
318
  let optionCharacters = "--";
@@ -328,10 +329,15 @@ function getOptionHelpEntry(
328
329
  subEntries = [];
329
330
  option.properties.forEach((property) => {
330
331
  subEntries!.push(
331
- getOptionHelpEntry(cliConfig, includeEnvVars, command, property, newSubEntryIndent, [
332
- ...optionAncestry,
333
- option,
334
- ]),
332
+ getOptionHelpEntry(
333
+ cliConfig,
334
+ includeEnvVars,
335
+ command,
336
+ property,
337
+ newSubEntryIndent,
338
+ [...optionAncestry, option],
339
+ groupCommand,
340
+ ),
335
341
  );
336
342
  });
337
343
  } else {
@@ -384,10 +390,12 @@ function getOptionHelpEntry(
384
390
  }
385
391
 
386
392
  if (includeEnvVars && option.type !== ComplexValueTypeName.COMPLEX) {
387
- const configurationKey = getSubCommandArgumentConfigurationKey(cliConfig, command, [
388
- ...(optionAncestry as Array<SubCommandArgument>),
389
- option as unknown as SubCommandArgument,
390
- ]);
393
+ const configurationKey = getSubCommandArgumentConfigurationKey(
394
+ cliConfig,
395
+ command,
396
+ [...(optionAncestry as Array<SubCommandArgument>), option as unknown as SubCommandArgument],
397
+ groupCommand,
398
+ );
391
399
  if (configurationKey !== undefined) {
392
400
  notesItems.push(`env var: ${configurationKey}`);
393
401
  }
@@ -418,6 +426,7 @@ function getPositionalHelpEntry(
418
426
  includeEnvVars: boolean,
419
427
  command: Command,
420
428
  positional: Positional,
429
+ groupCommand?: GroupCommand,
421
430
  ): HelpEntry {
422
431
  const notesItems: Array<string> = [];
423
432
  if (positional.allowableValues !== undefined && positional.allowableValues.length > 0) {
@@ -458,9 +467,12 @@ function getPositionalHelpEntry(
458
467
  }
459
468
 
460
469
  if (includeEnvVars) {
461
- const configurationKey = getSubCommandArgumentConfigurationKey(cliConfig, command, [
462
- positional,
463
- ]);
470
+ const configurationKey = getSubCommandArgumentConfigurationKey(
471
+ cliConfig,
472
+ command,
473
+ [positional],
474
+ groupCommand,
475
+ );
464
476
  if (configurationKey !== undefined) {
465
477
  notesItems.push(`env var: ${configurationKey}`);
466
478
  }
@@ -486,6 +498,7 @@ export function getCommandArgsHelpSections(
486
498
  includeEnvVars: boolean,
487
499
  subCommand: SubCommand,
488
500
  isSingleCommandApp: boolean,
501
+ groupCommand?: GroupCommand,
489
502
  ): Array<HelpSection> {
490
503
  const helpSections: Array<HelpSection> = [];
491
504
  if (subCommand.options.length > 0 || subCommand.positionals.length > 0) {
@@ -495,12 +508,20 @@ export function getCommandArgsHelpSections(
495
508
  };
496
509
  subCommand.options.forEach((option) => {
497
510
  argumentsSection.helpEntries.push(
498
- getOptionHelpEntry(cliConfig, includeEnvVars, subCommand, option as Option),
511
+ getOptionHelpEntry(
512
+ cliConfig,
513
+ includeEnvVars,
514
+ subCommand,
515
+ option as Option,
516
+ 0,
517
+ [],
518
+ groupCommand,
519
+ ),
499
520
  );
500
521
  });
501
522
  subCommand.positionals.forEach((positional) => {
502
523
  argumentsSection.helpEntries.push(
503
- getPositionalHelpEntry(cliConfig, includeEnvVars, subCommand, positional),
524
+ getPositionalHelpEntry(cliConfig, includeEnvVars, subCommand, positional, groupCommand),
504
525
  );
505
526
  });
506
527
  helpSections.push(argumentsSection);
@@ -1,5 +1,6 @@
1
1
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
+ import type { SecretAwareParseResult } from "../runtime/parser.ts";
3
4
  import type { ParseResult } from "../runtime/parser.ts";
4
5
  import { isGlobalCommand, isGlobalModifierCommand } from "../runtime/command/CommandTypeGuards.ts";
5
6
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
@@ -122,3 +123,20 @@ export async function printUnusedArgsWarning(
122
123
  await printerService.warn(`Unused args: ${overallUnusedArgs.join(" ")}\n\n`, Icon.ALERT);
123
124
  }
124
125
  }
126
+
127
+ export async function printSecretArgsWarning(
128
+ context: Context,
129
+ parseResult: SecretAwareParseResult,
130
+ ) {
131
+ const secretArgumentNames = parseResult.secretArgumentNames;
132
+ if (!secretArgumentNames || secretArgumentNames.length === 0) {
133
+ return;
134
+ }
135
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
136
+ await printerService.warn(
137
+ `SECRET value passed as a plain argument: ${secretArgumentNames.join(", ")}. ` +
138
+ "This is not recommended as it may be exposed via shell history and process listings. " +
139
+ "Use an environment variable, a prompt or a configuration file default instead.\n\n",
140
+ Icon.ALERT,
141
+ );
142
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ ValueTypeName,
3
+ type ComplexOption,
4
+ type GlobalCommand,
5
+ type Option,
6
+ type PopulatedSingleValueType,
7
+ type PopulatedValues,
8
+ type PopulatedValueType,
9
+ type SubCommand,
10
+ } from "@flowscripter/dynamic-cli-framework-api";
11
+ import { isComplexOption } from "../runtime/argument/ArgumentTypeGuards.ts";
12
+
13
+ export const REDACTED = "***";
14
+
15
+ type NamedArgument = { readonly name: string; readonly type?: unknown };
16
+
17
+ type RedactableValue =
18
+ | PopulatedValueType
19
+ | PopulatedValues
20
+ | Array<PopulatedValues | undefined>
21
+ | undefined;
22
+
23
+ function redactValue(
24
+ definition: NamedArgument | ComplexOption,
25
+ value: RedactableValue | PopulatedValues,
26
+ ): unknown {
27
+ if (value === undefined || value === null) {
28
+ return value;
29
+ }
30
+ if (isComplexOption(definition as ComplexOption)) {
31
+ const properties = (definition as ComplexOption).properties;
32
+ if (Array.isArray(value)) {
33
+ return value.map((v) => redactValue(definition, v));
34
+ }
35
+ return redactDefinedValues(properties, [], value as PopulatedValues);
36
+ }
37
+ if (definition.type === ValueTypeName.SECRET) {
38
+ return Array.isArray(value) ? value.map(() => REDACTED) : REDACTED;
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function redactDefinedValues(
44
+ options: ReadonlyArray<Option | ComplexOption>,
45
+ positionals: ReadonlyArray<NamedArgument>,
46
+ values: PopulatedValues,
47
+ ): PopulatedValues {
48
+ const redacted: Record<string, unknown> = {};
49
+ for (const [name, value] of Object.entries(values)) {
50
+ const definition =
51
+ options.find((o) => o.name === name) ?? positionals.find((p) => p.name === name);
52
+ redacted[name] = definition ? redactValue(definition, value) : value;
53
+ }
54
+ return redacted as PopulatedValues;
55
+ }
56
+
57
+ /**
58
+ * Copy of the provided values suitable for logging, with the values of any SECRET options or positionals replaced.
59
+ */
60
+ export function redactSubCommandValues<T extends PopulatedValues | undefined>(
61
+ subCommand: SubCommand,
62
+ values: T,
63
+ ): T {
64
+ if (values === undefined) {
65
+ return values;
66
+ }
67
+ return redactDefinedValues(subCommand.options, subCommand.positionals, values) as T;
68
+ }
69
+
70
+ /**
71
+ * Copy of the provided value suitable for logging, with the value replaced if the global command argument is a SECRET.
72
+ */
73
+ export function redactGlobalCommandValue(
74
+ globalCommand: GlobalCommand,
75
+ value: PopulatedSingleValueType,
76
+ ): PopulatedSingleValueType {
77
+ if (value !== undefined && globalCommand.argument?.type === ValueTypeName.SECRET) {
78
+ return REDACTED;
79
+ }
80
+ return value;
81
+ }