@flowscripter/dynamic-cli-framework 10.0.0 → 10.1.1

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 (29) hide show
  1. package/dist/src/runtime/parser.d.ts +8 -2
  2. package/dist/src/runtime/parser.d.ts.map +1 -1
  3. package/dist/src/runtime/parser.js +11 -6
  4. package/dist/src/runtime/runner.d.ts.map +1 -1
  5. package/dist/src/runtime/runner.js +4 -1
  6. package/dist/src/runtime/values/ValuePopulationResult.d.ts +4 -0
  7. package/dist/src/runtime/values/ValuePopulationResult.d.ts.map +1 -1
  8. package/dist/src/runtime/values/globalCommandValuePopulation.d.ts.map +1 -1
  9. package/dist/src/runtime/values/globalCommandValuePopulation.js +11 -3
  10. package/dist/src/runtime/values/subCommandValuePopulation.d.ts.map +1 -1
  11. package/dist/src/runtime/values/subCommandValuePopulation.js +39 -9
  12. package/dist/src/service/prompter/prompt/promptOpenUrl.d.ts +1 -0
  13. package/dist/src/service/prompter/prompt/promptOpenUrl.d.ts.map +1 -1
  14. package/dist/src/service/prompter/prompt/promptOpenUrl.js +10 -10
  15. package/dist/src/util/runnerHelper.d.ts +2 -0
  16. package/dist/src/util/runnerHelper.d.ts.map +1 -1
  17. package/dist/src/util/runnerHelper.js +10 -0
  18. package/dist/src/util/secretRedaction.d.ts +11 -0
  19. package/dist/src/util/secretRedaction.d.ts.map +1 -0
  20. package/dist/src/util/secretRedaction.js +45 -0
  21. package/package.json +1 -1
  22. package/src/runtime/parser.ts +22 -16
  23. package/src/runtime/runner.ts +4 -0
  24. package/src/runtime/values/ValuePopulationResult.ts +5 -0
  25. package/src/runtime/values/globalCommandValuePopulation.ts +12 -5
  26. package/src/runtime/values/subCommandValuePopulation.ts +47 -12
  27. package/src/service/prompter/prompt/promptOpenUrl.ts +11 -8
  28. package/src/util/runnerHelper.ts +18 -0
  29. package/src/util/secretRedaction.ts +81 -0
@@ -2,18 +2,24 @@ import type { PopulatedSingleValueType, PopulatedValues } from "@flowscripter/dy
2
2
  import type { ParseResult } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { CommandClause } from "./scanner.ts";
4
4
  export type { ParseResult } from "@flowscripter/dynamic-cli-framework-api";
5
+ /**
6
+ * A {@link ParseResult} which also reports any SECRET arguments supplied as plain command line arguments.
7
+ */
8
+ export type SecretAwareParseResult = ParseResult & {
9
+ readonly secretArgumentNames?: ReadonlyArray<string>;
10
+ };
5
11
  /**
6
12
  * Parse the arguments for the specified {@link CommandClause} assuming it contains a {@link SubCommand}.
7
13
  *
8
14
  * @param commandClause the clause to parse.
9
15
  * @param defaultValues optional default {@link PopulatedValues} to use for population before parsing the provided arguments.
10
16
  */
11
- export declare function parseSubCommandClause(commandClause: CommandClause, defaultValues?: PopulatedValues): ParseResult;
17
+ export declare function parseSubCommandClause(commandClause: CommandClause, defaultValues?: PopulatedValues): SecretAwareParseResult;
12
18
  /**
13
19
  * Parse the arguments for the specified {@link CommandClause} assuming it contains a {@link GlobalCommand} or {@link GlobalModifierCommand}.
14
20
  *
15
21
  * @param commandClause the clause to parse.
16
22
  * @param defaultValue optional default {@link PopulatedSingleValueType} to use for population before parsing the provided arguments.
17
23
  */
18
- export declare function parseGlobalCommandClause(commandClause: CommandClause, defaultValue?: PopulatedSingleValueType): ParseResult;
24
+ export declare function parseGlobalCommandClause(commandClause: CommandClause, defaultValue?: PopulatedSingleValueType): SecretAwareParseResult;
19
25
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/runtime/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,wBAAwB,EACxB,eAAe,EAChB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAclD,YAAY,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAE3E;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,aAAa,EAC5B,aAAa,CAAC,EAAE,eAAe,GAC9B,WAAW,CAiFb;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,EAC5B,YAAY,CAAC,EAAE,wBAAwB,GACtC,WAAW,CA0Db"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/runtime/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,wBAAwB,EACxB,eAAe,EAChB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAelD,YAAY,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAE3E;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG;IACjD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,aAAa,EAC5B,aAAa,CAAC,EAAE,eAAe,GAC9B,sBAAsB,CAiFxB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,EAC5B,YAAY,CAAC,EAAE,wBAAwB,GACtC,sBAAsB,CAwDxB"}
@@ -1,6 +1,7 @@
1
1
  import populateGlobalCommandValue from "./values/globalCommandValuePopulation.js";
2
2
  import { validateGlobalCommandArgumentValue, validateOptionValue, validatePositionalValue, } from "./values/argumentValueValidation.js";
3
3
  import getLogger from "../util/logger.js";
4
+ import { redactGlobalCommandValue, redactSubCommandValues } from "../util/secretRedaction.js";
4
5
  import populateSubCommandValues from "./values/subCommandValuePopulation.js";
5
6
  const logger = getLogger("parser");
6
7
  /**
@@ -24,7 +25,7 @@ export function parseSubCommandClause(commandClause, defaultValues) {
24
25
  unusedArgs: potentialArgs,
25
26
  };
26
27
  }
27
- const { populatedArgumentValues, unusedArgs, invalidArgument } = populateSubCommandValues(command, potentialArgs, defaultValues);
28
+ const { populatedArgumentValues, unusedArgs, invalidArgument, secretArgumentNames } = populateSubCommandValues(command, potentialArgs, defaultValues);
28
29
  if (invalidArgument) {
29
30
  invalidArguments.push(invalidArgument);
30
31
  }
@@ -36,7 +37,7 @@ export function parseSubCommandClause(commandClause, defaultValues) {
36
37
  populatedArgumentValues[option.name] = validatedValue;
37
38
  }
38
39
  });
39
- logger.debug("Command arguments after options validated: %O with invalid args: %O", populatedArgumentValues, invalidArguments);
40
+ logger.debug("Command arguments after options validated: %O with invalid args: %O", redactSubCommandValues(command, populatedArgumentValues), invalidArguments);
40
41
  if (invalidArguments.length === 0) {
41
42
  // validate the positional values
42
43
  command.positionals.forEach((positional) => {
@@ -45,9 +46,10 @@ export function parseSubCommandClause(commandClause, defaultValues) {
45
46
  populatedArgumentValues[positional.name] = validatedValue;
46
47
  }
47
48
  });
48
- logger.debug("Command arguments after positionals validated: %O with invalid args: %O", populatedArgumentValues, invalidArguments);
49
+ logger.debug("Command arguments after positionals validated: %O with invalid args: %O", redactSubCommandValues(command, populatedArgumentValues), invalidArguments);
49
50
  }
50
51
  }
52
+ const secrets = secretArgumentNames ? { secretArgumentNames } : {};
51
53
  if (groupCommand) {
52
54
  return {
53
55
  command,
@@ -55,6 +57,7 @@ export function parseSubCommandClause(commandClause, defaultValues) {
55
57
  groupCommand,
56
58
  invalidArguments,
57
59
  unusedArgs,
60
+ ...secrets,
58
61
  };
59
62
  }
60
63
  return {
@@ -62,6 +65,7 @@ export function parseSubCommandClause(commandClause, defaultValues) {
62
65
  populatedArgumentValues,
63
66
  invalidArguments,
64
67
  unusedArgs: unusedArgs,
68
+ ...secrets,
65
69
  };
66
70
  }
67
71
  /**
@@ -83,11 +87,11 @@ export function parseGlobalCommandClause(commandClause, defaultValue) {
83
87
  unusedArgs: potentialArgs,
84
88
  };
85
89
  }
86
- let { populatedArgumentValue, unusedArgs, invalidArgument } = populateGlobalCommandValue(command, potentialArgs, defaultValue);
90
+ let { populatedArgumentValue, unusedArgs, invalidArgument, secretArgumentNames } = populateGlobalCommandValue(command, potentialArgs, defaultValue);
87
91
  if (invalidArgument) {
88
92
  invalidArguments.push(invalidArgument);
89
93
  }
90
- logger.debug("Command arguments for command: %s after value population: %O with invalid args: %O and unused args: %O", command.name, populatedArgumentValue, invalidArguments, unusedArgs);
94
+ logger.debug("Command arguments for command: %s after value population: %O with invalid args: %O and unused args: %O", command.name, redactGlobalCommandValue(command, populatedArgumentValue), invalidArguments, unusedArgs);
91
95
  // don't validate the value if the argument is already invalid
92
96
  if (invalidArguments.length === 0) {
93
97
  const validatedValue = validateGlobalCommandArgumentValue(command, populatedArgumentValue, invalidArguments);
@@ -95,11 +99,12 @@ export function parseGlobalCommandClause(commandClause, defaultValue) {
95
99
  populatedArgumentValue = validatedValue;
96
100
  }
97
101
  }
98
- logger.debug("Command arguments for command: %s after value validation: %O with invalid args: %O", command.name, populatedArgumentValue, invalidArguments);
102
+ logger.debug("Command arguments for command: %s after value validation: %O with invalid args: %O", command.name, redactGlobalCommandValue(command, populatedArgumentValue), invalidArguments);
99
103
  return {
100
104
  command,
101
105
  populatedArgumentValues: populatedArgumentValue,
102
106
  invalidArguments,
103
107
  unusedArgs,
108
+ ...(secretArgumentNames && { secretArgumentNames }),
104
109
  };
105
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAiB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;AAMhF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAmfzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,wBAAsB,GAAG,CACvB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,eAAe,EAAE,eAAe,EAChC,uBAAuB,EAAE,uBAAuB,EAChD,4BAA4B,EAAE,4BAA4B,GAAG,SAAS,EACtE,uBAAuB,EAAE,uBAAuB,GAAG,SAAS,EAC5D,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,OAAO,EACxB,cAAc,CAAC,EAAE,qBAAqB,GACrC,OAAO,CAAC,YAAY,CAAC,CAkKvB"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAkB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;AAMhF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAsfzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,wBAAsB,GAAG,CACvB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,eAAe,EAAE,eAAe,EAChC,uBAAuB,EAAE,uBAAuB,EAChD,4BAA4B,EAAE,4BAA4B,GAAG,SAAS,EACtE,uBAAuB,EAAE,uBAAuB,GAAG,SAAS,EAC5D,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,OAAO,EACxB,cAAc,CAAC,EAAE,qBAAqB,GACrC,OAAO,CAAC,YAAY,CAAC,CAkKvB"}
@@ -1,7 +1,7 @@
1
1
  import { RunState } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { isGlobalCommand, isSubCommand } from "./command/CommandTypeGuards.js";
3
3
  import getLogger from "../util/logger.js";
4
- import { printCommandExecutionError, printNoCommandRecognisedError, printNoCommandSpecifiedError, printParseResultError, printUnusedArgsWarning, } from "../util/runnerHelper.js";
4
+ import { printCommandExecutionError, printNoCommandRecognisedError, printNoCommandSpecifiedError, printParseResultError, printSecretArgsWarning, printUnusedArgsWarning, } from "../util/runnerHelper.js";
5
5
  import { scanForGlobalModifierCommandClauses, scanForNonModifierCommandClause } from "./scanner.js";
6
6
  import { parseGlobalCommandClause, parseSubCommandClause } from "./parser.js";
7
7
  import { ARGUMENT_PROMPTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
@@ -113,6 +113,7 @@ async function findAndExecuteGlobalModifierCommands(availableArgSequences, unuse
113
113
  ? await configurationServiceProvider.getDefaultArgumentValues(context.cliConfig, globalModifierCommandClause.command)
114
114
  : undefined;
115
115
  const parseResult = parseGlobalCommandClause(globalModifierCommandClause, defaultArgumentValues);
116
+ await printSecretArgsWarning(context, parseResult);
116
117
  if (parseResult.invalidArguments.length > 0) {
117
118
  await printParseResultError(context, parseResult);
118
119
  return {
@@ -194,6 +195,7 @@ async function findAndExecuteNonModifierCommand(availableArgs, unusedArgs, nonMo
194
195
  else {
195
196
  parseResult = parseSubCommandClause(scanResult.nonModifierCommandClause, defaultArgumentValues);
196
197
  }
198
+ await printSecretArgsWarning(context, parseResult);
197
199
  if (parseResult.invalidArguments.length > 0) {
198
200
  const prompted = await attemptPromptForMissingArguments(parseResult, context);
199
201
  if (prompted) {
@@ -298,6 +300,7 @@ async function findAndExecuteDefaultNonModifierCommand(availableArgs, unusedArgs
298
300
  potentialArgs: [...argSequence],
299
301
  }, defaultArgumentValues);
300
302
  }
303
+ await printSecretArgsWarning(context, parseResult);
301
304
  // add any unused args back to the list of still unused args
302
305
  if (parseResult.unusedArgs.length > 0) {
303
306
  unusedArgs.push(parseResult.unusedArgs);
@@ -12,6 +12,10 @@ export interface ValuePopulationResult {
12
12
  * Any invalid argument discovered while populating values.
13
13
  */
14
14
  readonly invalidArgument?: InvalidArgument;
15
+ /**
16
+ * Names of any SECRET arguments whose values were supplied as plain command line arguments.
17
+ */
18
+ readonly secretArgumentNames?: ReadonlyArray<string>;
15
19
  }
16
20
  /**
17
21
  * A container holding the result of argument values population for a {@link SubCommand}.
@@ -1 +1 @@
1
- {"version":3,"file":"ValuePopulationResult.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/ValuePopulationResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,eAAe,EAChB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAE3C;;OAEG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,+BAAgC,SAAQ,qBAAqB;IAC5E;;OAEG;IACH,QAAQ,CAAC,uBAAuB,EAAE,eAAe,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,kCAAmC,SAAQ,qBAAqB;IAC/E;;OAEG;IACH,QAAQ,CAAC,sBAAsB,EAAE,wBAAwB,CAAC;CAC3D"}
1
+ {"version":3,"file":"ValuePopulationResult.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/ValuePopulationResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,eAAe,EAChB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAE3C;;OAEG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAE3C;;OAEG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtD;AAED;;GAEG;AACH,MAAM,WAAW,+BAAgC,SAAQ,qBAAqB;IAC5E;;OAEG;IACH,QAAQ,CAAC,uBAAuB,EAAE,eAAe,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,kCAAmC,SAAQ,qBAAqB;IAC/E;;OAEG;IACH,QAAQ,CAAC,sBAAsB,EAAE,wBAAwB,CAAC;CAC3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"globalCommandValuePopulation.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/globalCommandValuePopulation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,4BAA4B,CAAC;AAErF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AAMxF;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAChD,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,YAAY,EAAE,wBAAwB,GACrC,kCAAkC,CAkEpC"}
1
+ {"version":3,"file":"globalCommandValuePopulation.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/globalCommandValuePopulation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,kCAAkC,EAAE,MAAM,4BAA4B,CAAC;AAErF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yCAAyC,CAAC;AAOxF;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAChD,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,YAAY,EAAE,wBAAwB,GACrC,kCAAkC,CAwEpC"}
@@ -1,5 +1,6 @@
1
1
  import { ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import getLogger from "../../util/logger.js";
3
+ import { REDACTED } from "../../util/secretRedaction.js";
3
4
  import { InvalidArgumentReason } from "@flowscripter/dynamic-cli-framework-api";
4
5
  const logger = getLogger("globalCommandValuePopulation");
5
6
  /**
@@ -10,14 +11,20 @@ const logger = getLogger("globalCommandValuePopulation");
10
11
  * @param defaultValue optional default value to use for population before parsing the provided arguments.
11
12
  */
12
13
  export default function populateGlobalCommandValue(globalCommand, potentialArgs, defaultValue) {
14
+ const argument = globalCommand.argument;
15
+ const isSecret = argument.type === ValueTypeName.SECRET;
13
16
  logger.debug(() => {
14
- const message = `Populating value for global command: '${globalCommand.name}' using potential args: ${potentialArgs.join(" ")}`;
17
+ const message = `Populating value for global command: '${globalCommand.name}' using potential args: ${(isSecret
18
+ ? potentialArgs
19
+ .slice(0, 1)
20
+ .map(() => REDACTED)
21
+ .concat(potentialArgs.slice(1))
22
+ : potentialArgs).join(" ")}`;
15
23
  if (defaultValue !== undefined) {
16
- return `${message} and configured value: '${defaultValue}'`;
24
+ return `${message} and configured value: '${isSecret ? REDACTED : defaultValue}'`;
17
25
  }
18
26
  return message;
19
27
  });
20
- const argument = globalCommand.argument;
21
28
  // default to defined argument default value
22
29
  let populatedArgumentValue = argument.defaultValue;
23
30
  // override with provided configured value
@@ -63,5 +70,6 @@ export default function populateGlobalCommandValue(globalCommand, potentialArgs,
63
70
  return {
64
71
  populatedArgumentValue,
65
72
  unusedArgs,
73
+ ...(isSecret && potentialArgs.length > 0 && { secretArgumentNames: [globalCommand.name] }),
66
74
  };
67
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"subCommandValuePopulation.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/subCommandValuePopulation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,yCAAyC,CAAC;AAGjD,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAsjBlF;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,UAAU,EAAE,UAAU,EACtB,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,aAAa,EAAE,eAAe,GAAG,SAAS,GACzC,+BAA+B,CAkEjC"}
1
+ {"version":3,"file":"subCommandValuePopulation.d.ts","sourceRoot":"","sources":["../../../../src/runtime/values/subCommandValuePopulation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,yCAAyC,CAAC;AAIjD,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AA2kBlF;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,UAAU,EAAE,UAAU,EACtB,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,EACpC,aAAa,EAAE,eAAe,GAAG,SAAS,GACzC,+BAA+B,CA+EjC"}
@@ -1,5 +1,6 @@
1
1
  import { ValueTypeName, } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import getLogger from "../../util/logger.js";
3
+ import { REDACTED, redactSubCommandValues } from "../../util/secretRedaction.js";
3
4
  import argumentValueMerge from "./argumentValueMerge.js";
4
5
  import { MAXIMUM_COMPLEX_OPTION_NESTING_DEPTH } from "@flowscripter/dynamic-cli-framework-api";
5
6
  import { isComplexOption } from "../argument/ArgumentTypeGuards.js";
@@ -62,6 +63,18 @@ class ParseContext {
62
63
  * which will be populated with a positional value if found.
63
64
  */
64
65
  currentPositionalIndex;
66
+ /**
67
+ * Set when the most recent argument supplied a value for a SECRET option or positional.
68
+ */
69
+ secretArgument;
70
+ /**
71
+ * Return and clear the {@link secretArgument} recorded by the most recent {@link parse}.
72
+ */
73
+ takeSecretArgument() {
74
+ const secretArgument = this.secretArgument;
75
+ this.secretArgument = undefined;
76
+ return secretArgument;
77
+ }
65
78
  /**
66
79
  * Use to store lazily created map of option paths e.g. --alpha.b.g and -a.b.gamma for {@link ComplexOption} or {@link Option} instances.
67
80
  */
@@ -294,7 +307,10 @@ class ParseContext {
294
307
  * @return `false` if there is a problem populating the value, in which case the {@link state} will be set to
295
308
  * {@link ParseState.ERROR} and {@link invalidArgument} will be populated.
296
309
  */
297
- setOptionValue(value) {
310
+ setOptionValue(value, inline = false) {
311
+ if (this.currentOption.type === ValueTypeName.SECRET) {
312
+ this.secretArgument = { name: this.currentOption.name, inline };
313
+ }
298
314
  let optionValue = this.populatedArgumentValues;
299
315
  const optionPath = this.currentOptionPath;
300
316
  // traverse the current populated values using the validated option path elements
@@ -367,6 +383,9 @@ class ParseContext {
367
383
  setPositionalValue(value) {
368
384
  const positional = this.subCommand.positionals[this.currentPositionalIndex];
369
385
  const currentValue = this.populatedArgumentValues[positional.name];
386
+ if (positional.type === ValueTypeName.SECRET) {
387
+ this.secretArgument = { name: positional.name, inline: false };
388
+ }
370
389
  // if we haven't populated the argument value before...
371
390
  if (currentValue === undefined) {
372
391
  if (positional.isVarargMultiple) {
@@ -460,7 +479,7 @@ class ParseContext {
460
479
  // try to set the value if we have one
461
480
  if (potentialOptionValue !== undefined) {
462
481
  // set the value
463
- this.setOptionValue(potentialOptionValue);
482
+ this.setOptionValue(potentialOptionValue, true);
464
483
  // return regardless
465
484
  return;
466
485
  }
@@ -491,19 +510,21 @@ class ParseContext {
491
510
  * @param defaultValues optional default {@link Values} to use for population before parsing the provided arguments.
492
511
  */
493
512
  export default function populateSubCommandValues(subCommand, potentialArgs, defaultValues) {
494
- logger.debug(() => {
495
- const message = `Populating values for sub-command: '${subCommand.name}' using potential args: ${potentialArgs.join(" ")}`;
496
- if (defaultValues !== undefined) {
497
- return `${message} and configured values: ${JSON.stringify(defaultValues)}`;
498
- }
499
- return message;
500
- });
501
513
  const parseContext = new ParseContext(subCommand);
502
514
  const unusedArgs = [];
515
+ const loggedArgs = [...potentialArgs];
516
+ const secretArgumentNames = new Set();
503
517
  for (let i = 0; i < potentialArgs.length; i++) {
504
518
  const potentialArg = potentialArgs[i];
505
519
  // parse the next arg
506
520
  parseContext.parse(potentialArg);
521
+ const secretArgument = parseContext.takeSecretArgument();
522
+ if (secretArgument) {
523
+ secretArgumentNames.add(secretArgument.name);
524
+ loggedArgs[i] = secretArgument.inline
525
+ ? `${potentialArg.slice(0, potentialArg.indexOf("=") + 1)}${REDACTED}`
526
+ : REDACTED;
527
+ }
507
528
  // check if we had a parsing error, record the error, flush the remaining args and stop parsing
508
529
  if (parseContext.state === ParseState.ERROR) {
509
530
  if (i + 1 < potentialArgs.length) {
@@ -517,6 +538,13 @@ export default function populateSubCommandValues(subCommand, potentialArgs, defa
517
538
  parseContext.state = ParseState.EMPTY;
518
539
  }
519
540
  }
541
+ logger.debug(() => {
542
+ const message = `Populated values for sub-command: '${subCommand.name}' using potential args: ${loggedArgs.join(" ")}`;
543
+ if (defaultValues !== undefined) {
544
+ return `${message} and configured values: ${JSON.stringify(redactSubCommandValues(subCommand, defaultValues))}`;
545
+ }
546
+ return message;
547
+ });
520
548
  // Check if the last arg specified an option but no value
521
549
  if (parseContext.state === ParseState.OPTION_VALUE_EXPECTED) {
522
550
  // special case if the last arg was for a boolean option where the value was implicitly `true`
@@ -536,6 +564,7 @@ export default function populateSubCommandValues(subCommand, potentialArgs, defa
536
564
  populatedArgumentValues: parseContext.populatedArgumentValues,
537
565
  unusedArgs: unusedArgs,
538
566
  invalidArgument: parseContext.invalidArgument,
567
+ ...(secretArgumentNames.size > 0 && { secretArgumentNames: [...secretArgumentNames] }),
539
568
  };
540
569
  }
541
570
  // merge the configured values with the parsed values
@@ -546,5 +575,6 @@ export default function populateSubCommandValues(subCommand, potentialArgs, defa
546
575
  return {
547
576
  populatedArgumentValues,
548
577
  unusedArgs: unusedArgs,
578
+ ...(secretArgumentNames.size > 0 && { secretArgumentNames: [...secretArgumentNames] }),
549
579
  };
550
580
  }
@@ -1,5 +1,6 @@
1
1
  import type { Prompt, PromptResult, SpawnService } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import type { PromptContext } from "./PromptContext.ts";
3
+ export declare function getOpenCommand(platform: string, url: string): string[];
3
4
  export declare function defaultOpenUrl(spawnService: SpawnService, url: string): Promise<void>;
4
5
  export default function promptOpenUrl(ctx: PromptContext, promptDef: Prompt): Promise<PromptResult>;
5
6
  //# sourceMappingURL=promptOpenUrl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"promptOpenUrl.d.ts","sourceRoot":"","sources":["../../../../../src/service/prompter/prompt/promptOpenUrl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAGlG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,wBAAsB,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3F;AAED,wBAA8B,aAAa,CACzC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,CAkDvB"}
1
+ {"version":3,"file":"promptOpenUrl.d.ts","sourceRoot":"","sources":["../../../../../src/service/prompter/prompt/promptOpenUrl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAGlG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAQtE;AAED,wBAAsB,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB3F;AAED,wBAA8B,aAAa,CACzC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,CAkDvB"}
@@ -3,21 +3,21 @@ import { getSpawnService, interrupt, renderPromptHeader } from "./PromptContext.
3
3
  function isRemoteSession() {
4
4
  return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY);
5
5
  }
6
+ export function getOpenCommand(platform, url) {
7
+ if (platform === "darwin") {
8
+ return ["open", url];
9
+ }
10
+ if (platform === "win32") {
11
+ return ["rundll32", "url.dll,FileProtocolHandler", url];
12
+ }
13
+ return ["xdg-open", url];
14
+ }
6
15
  export async function defaultOpenUrl(spawnService, url) {
7
16
  const parsed = new URL(url);
8
17
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
9
18
  throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
10
19
  }
11
- let cmd;
12
- if (process.platform === "darwin") {
13
- cmd = ["open", url];
14
- }
15
- else if (process.platform === "win32") {
16
- cmd = ["cmd", "/c", "start", "", url];
17
- }
18
- else {
19
- cmd = ["xdg-open", url];
20
- }
20
+ const cmd = getOpenCommand(process.platform, parsed.href);
21
21
  const result = await spawnService.spawn(cmd, { mode: "ignore", longRunning: false });
22
22
  if (result.ok) {
23
23
  return;
@@ -1,3 +1,4 @@
1
+ import type { SecretAwareParseResult } from "../runtime/parser.ts";
1
2
  import type { ParseResult } from "../runtime/parser.ts";
2
3
  import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
4
  import type { CommandRegistry } from "@flowscripter/dynamic-cli-framework-api";
@@ -7,4 +8,5 @@ export declare function printRuntimeError(context: Context, err: Error): Promise
7
8
  export declare function printNoCommandSpecifiedError(context: Context): Promise<void>;
8
9
  export declare function printNoCommandRecognisedError(context: Context, commandRegistry: CommandRegistry, availableArgs: ReadonlyArray<string>): Promise<void>;
9
10
  export declare function printUnusedArgsWarning(context: Context, overallUnusedArgs: ReadonlyArray<string>): Promise<void>;
11
+ export declare function printSecretArgsWarning(context: Context, parseResult: SecretAwareParseResult): Promise<void>;
10
12
  //# sourceMappingURL=runnerHelper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runnerHelper.d.ts","sourceRoot":"","sources":["../../../src/util/runnerHelper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAY/E,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,WAAW,EACxB,gBAAgB,UAAQ,iBAiBzB;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,KAAK,EACV,gBAAgB,UAAQ,iBAiBzB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,iBAMnE;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,OAAO,iBAGlE;AAED,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,iBAgCrC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAAC,iBAQzC"}
1
+ {"version":3,"file":"runnerHelper.d.ts","sourceRoot":"","sources":["../../../src/util/runnerHelper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAEvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAY/E,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,WAAW,EACxB,gBAAgB,UAAQ,iBAiBzB;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,KAAK,EACV,gBAAgB,UAAQ,iBAiBzB;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,iBAMnE;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,OAAO,iBAGlE;AAED,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,iBAgCrC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,EAChB,iBAAiB,EAAE,aAAa,CAAC,MAAM,CAAC,iBAQzC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,sBAAsB,iBAapC"}
@@ -78,3 +78,13 @@ export async function printUnusedArgsWarning(context, overallUnusedArgs) {
78
78
  await printerService.warn(`Unused args: ${overallUnusedArgs.join(" ")}\n\n`, Icon.ALERT);
79
79
  }
80
80
  }
81
+ export async function printSecretArgsWarning(context, parseResult) {
82
+ const secretArgumentNames = parseResult.secretArgumentNames;
83
+ if (!secretArgumentNames || secretArgumentNames.length === 0) {
84
+ return;
85
+ }
86
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
87
+ await printerService.warn(`SECRET value passed as a plain argument: ${secretArgumentNames.join(", ")}. ` +
88
+ "This is not recommended as it may be exposed via shell history and process listings. " +
89
+ "Use an environment variable, a prompt or a configuration file default instead.\n\n", Icon.ALERT);
90
+ }
@@ -0,0 +1,11 @@
1
+ import { type GlobalCommand, type PopulatedSingleValueType, type PopulatedValues, type SubCommand } from "@flowscripter/dynamic-cli-framework-api";
2
+ export declare const REDACTED = "***";
3
+ /**
4
+ * Copy of the provided values suitable for logging, with the values of any SECRET options or positionals replaced.
5
+ */
6
+ export declare function redactSubCommandValues<T extends PopulatedValues | undefined>(subCommand: SubCommand, values: T): T;
7
+ /**
8
+ * Copy of the provided value suitable for logging, with the value replaced if the global command argument is a SECRET.
9
+ */
10
+ export declare function redactGlobalCommandValue(globalCommand: GlobalCommand, value: PopulatedSingleValueType): PopulatedSingleValueType;
11
+ //# sourceMappingURL=secretRedaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secretRedaction.d.ts","sourceRoot":"","sources":["../../../src/util/secretRedaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAElB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EAEpB,KAAK,UAAU,EAChB,MAAM,yCAAyC,CAAC;AAGjD,eAAO,MAAM,QAAQ,QAAQ,CAAC;AA4C9B;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,eAAe,GAAG,SAAS,EAC1E,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,CAAC,GACR,CAAC,CAKH;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,wBAAwB,GAC9B,wBAAwB,CAK1B"}
@@ -0,0 +1,45 @@
1
+ import { ValueTypeName, } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { isComplexOption } from "../runtime/argument/ArgumentTypeGuards.js";
3
+ export const REDACTED = "***";
4
+ function redactValue(definition, value) {
5
+ if (value === undefined || value === null) {
6
+ return value;
7
+ }
8
+ if (isComplexOption(definition)) {
9
+ const properties = definition.properties;
10
+ if (Array.isArray(value)) {
11
+ return value.map((v) => redactValue(definition, v));
12
+ }
13
+ return redactDefinedValues(properties, [], value);
14
+ }
15
+ if (definition.type === ValueTypeName.SECRET) {
16
+ return Array.isArray(value) ? value.map(() => REDACTED) : REDACTED;
17
+ }
18
+ return value;
19
+ }
20
+ function redactDefinedValues(options, positionals, values) {
21
+ const redacted = {};
22
+ for (const [name, value] of Object.entries(values)) {
23
+ const definition = options.find((o) => o.name === name) ?? positionals.find((p) => p.name === name);
24
+ redacted[name] = definition ? redactValue(definition, value) : value;
25
+ }
26
+ return redacted;
27
+ }
28
+ /**
29
+ * Copy of the provided values suitable for logging, with the values of any SECRET options or positionals replaced.
30
+ */
31
+ export function redactSubCommandValues(subCommand, values) {
32
+ if (values === undefined) {
33
+ return values;
34
+ }
35
+ return redactDefinedValues(subCommand.options, subCommand.positionals, values);
36
+ }
37
+ /**
38
+ * Copy of the provided value suitable for logging, with the value replaced if the global command argument is a SECRET.
39
+ */
40
+ export function redactGlobalCommandValue(globalCommand, value) {
41
+ if (value !== undefined && globalCommand.argument?.type === ValueTypeName.SECRET) {
42
+ return REDACTED;
43
+ }
44
+ return value;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "10.0.0",
3
+ "version": "10.1.1",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -13,6 +13,7 @@ import {
13
13
  validatePositionalValue,
14
14
  } from "./values/argumentValueValidation.ts";
15
15
  import getLogger from "../util/logger.ts";
16
+ import { redactGlobalCommandValue, redactSubCommandValues } from "../util/secretRedaction.ts";
16
17
  import populateSubCommandValues from "./values/subCommandValuePopulation.ts";
17
18
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
18
19
  import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
@@ -21,6 +22,13 @@ const logger = getLogger("parser");
21
22
 
22
23
  export type { ParseResult } from "@flowscripter/dynamic-cli-framework-api";
23
24
 
25
+ /**
26
+ * A {@link ParseResult} which also reports any SECRET arguments supplied as plain command line arguments.
27
+ */
28
+ export type SecretAwareParseResult = ParseResult & {
29
+ readonly secretArgumentNames?: ReadonlyArray<string>;
30
+ };
31
+
24
32
  /**
25
33
  * Parse the arguments for the specified {@link CommandClause} assuming it contains a {@link SubCommand}.
26
34
  *
@@ -30,7 +38,7 @@ export type { ParseResult } from "@flowscripter/dynamic-cli-framework-api";
30
38
  export function parseSubCommandClause(
31
39
  commandClause: CommandClause,
32
40
  defaultValues?: PopulatedValues,
33
- ): ParseResult {
41
+ ): SecretAwareParseResult {
34
42
  const command = commandClause.command as SubCommand;
35
43
  const potentialArgs = commandClause.potentialArgs;
36
44
  const groupCommand = commandClause.groupCommand;
@@ -48,11 +56,8 @@ export function parseSubCommandClause(
48
56
  };
49
57
  }
50
58
 
51
- const { populatedArgumentValues, unusedArgs, invalidArgument } = populateSubCommandValues(
52
- command,
53
- potentialArgs,
54
- defaultValues,
55
- );
59
+ const { populatedArgumentValues, unusedArgs, invalidArgument, secretArgumentNames } =
60
+ populateSubCommandValues(command, potentialArgs, defaultValues);
56
61
 
57
62
  if (invalidArgument) {
58
63
  invalidArguments.push(invalidArgument);
@@ -73,7 +78,7 @@ export function parseSubCommandClause(
73
78
 
74
79
  logger.debug(
75
80
  "Command arguments after options validated: %O with invalid args: %O",
76
- populatedArgumentValues,
81
+ redactSubCommandValues(command, populatedArgumentValues),
77
82
  invalidArguments,
78
83
  );
79
84
 
@@ -91,11 +96,12 @@ export function parseSubCommandClause(
91
96
  });
92
97
  logger.debug(
93
98
  "Command arguments after positionals validated: %O with invalid args: %O",
94
- populatedArgumentValues,
99
+ redactSubCommandValues(command, populatedArgumentValues),
95
100
  invalidArguments,
96
101
  );
97
102
  }
98
103
  }
104
+ const secrets = secretArgumentNames ? { secretArgumentNames } : {};
99
105
  if (groupCommand) {
100
106
  return {
101
107
  command,
@@ -103,6 +109,7 @@ export function parseSubCommandClause(
103
109
  groupCommand,
104
110
  invalidArguments,
105
111
  unusedArgs,
112
+ ...secrets,
106
113
  };
107
114
  }
108
115
  return {
@@ -110,6 +117,7 @@ export function parseSubCommandClause(
110
117
  populatedArgumentValues,
111
118
  invalidArguments,
112
119
  unusedArgs: unusedArgs,
120
+ ...secrets,
113
121
  };
114
122
  }
115
123
 
@@ -122,7 +130,7 @@ export function parseSubCommandClause(
122
130
  export function parseGlobalCommandClause(
123
131
  commandClause: CommandClause,
124
132
  defaultValue?: PopulatedSingleValueType,
125
- ): ParseResult {
133
+ ): SecretAwareParseResult {
126
134
  const command = commandClause.command as GlobalCommand;
127
135
  const potentialArgs = commandClause.potentialArgs;
128
136
  const invalidArguments: Array<InvalidArgument> = [];
@@ -137,11 +145,8 @@ export function parseGlobalCommandClause(
137
145
  };
138
146
  }
139
147
 
140
- let { populatedArgumentValue, unusedArgs, invalidArgument } = populateGlobalCommandValue(
141
- command,
142
- potentialArgs,
143
- defaultValue,
144
- );
148
+ let { populatedArgumentValue, unusedArgs, invalidArgument, secretArgumentNames } =
149
+ populateGlobalCommandValue(command, potentialArgs, defaultValue);
145
150
 
146
151
  if (invalidArgument) {
147
152
  invalidArguments.push(invalidArgument);
@@ -150,7 +155,7 @@ export function parseGlobalCommandClause(
150
155
  logger.debug(
151
156
  "Command arguments for command: %s after value population: %O with invalid args: %O and unused args: %O",
152
157
  command.name,
153
- populatedArgumentValue,
158
+ redactGlobalCommandValue(command, populatedArgumentValue),
154
159
  invalidArguments,
155
160
  unusedArgs,
156
161
  );
@@ -170,7 +175,7 @@ export function parseGlobalCommandClause(
170
175
  logger.debug(
171
176
  "Command arguments for command: %s after value validation: %O with invalid args: %O",
172
177
  command.name,
173
- populatedArgumentValue,
178
+ redactGlobalCommandValue(command, populatedArgumentValue),
174
179
  invalidArguments,
175
180
  );
176
181
 
@@ -179,5 +184,6 @@ export function parseGlobalCommandClause(
179
184
  populatedArgumentValues: populatedArgumentValue,
180
185
  invalidArguments,
181
186
  unusedArgs,
187
+ ...(secretArgumentNames && { secretArgumentNames }),
182
188
  };
183
189
  }
@@ -15,6 +15,7 @@ import {
15
15
  printNoCommandRecognisedError,
16
16
  printNoCommandSpecifiedError,
17
17
  printParseResultError,
18
+ printSecretArgsWarning,
18
19
  printUnusedArgsWarning,
19
20
  } from "../util/runnerHelper.ts";
20
21
  import { scanForGlobalModifierCommandClauses, scanForNonModifierCommandClause } from "./scanner.ts";
@@ -198,6 +199,7 @@ async function findAndExecuteGlobalModifierCommands(
198
199
  globalModifierCommandClause,
199
200
  defaultArgumentValues as PopulatedSingleValueType,
200
201
  );
202
+ await printSecretArgsWarning(context, parseResult);
201
203
 
202
204
  if (parseResult.invalidArguments.length > 0) {
203
205
  await printParseResultError(context, parseResult);
@@ -329,6 +331,7 @@ async function findAndExecuteNonModifierCommand(
329
331
  defaultArgumentValues as PopulatedValues,
330
332
  );
331
333
  }
334
+ await printSecretArgsWarning(context, parseResult);
332
335
 
333
336
  if (parseResult.invalidArguments.length > 0) {
334
337
  const prompted = await attemptPromptForMissingArguments(parseResult, context);
@@ -464,6 +467,7 @@ async function findAndExecuteDefaultNonModifierCommand(
464
467
  defaultArgumentValues as PopulatedValues,
465
468
  );
466
469
  }
470
+ await printSecretArgsWarning(context, parseResult);
467
471
 
468
472
  // add any unused args back to the list of still unused args
469
473
  if (parseResult.unusedArgs.length > 0) {
@@ -17,6 +17,11 @@ export interface ValuePopulationResult {
17
17
  * Any invalid argument discovered while populating values.
18
18
  */
19
19
  readonly invalidArgument?: InvalidArgument;
20
+
21
+ /**
22
+ * Names of any SECRET arguments whose values were supplied as plain command line arguments.
23
+ */
24
+ readonly secretArgumentNames?: ReadonlyArray<string>;
20
25
  }
21
26
 
22
27
  /**
@@ -3,6 +3,7 @@ import type { GlobalCommandValuePopulationResult } from "./ValuePopulationResult
3
3
  import { ValueTypeName } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import type { PopulatedSingleValueType } from "@flowscripter/dynamic-cli-framework-api";
5
5
  import getLogger from "../../util/logger.ts";
6
+ import { REDACTED } from "../../util/secretRedaction.ts";
6
7
  import { InvalidArgumentReason } from "@flowscripter/dynamic-cli-framework-api";
7
8
 
8
9
  const logger = getLogger("globalCommandValuePopulation");
@@ -19,16 +20,21 @@ export default function populateGlobalCommandValue(
19
20
  potentialArgs: ReadonlyArray<string>,
20
21
  defaultValue: PopulatedSingleValueType,
21
22
  ): GlobalCommandValuePopulationResult {
23
+ const argument = globalCommand.argument!;
24
+ const isSecret = argument.type === ValueTypeName.SECRET;
22
25
  logger.debug(() => {
23
- const message = `Populating value for global command: '${globalCommand.name}' using potential args: ${potentialArgs.join(
24
- " ",
25
- )}`;
26
+ const message = `Populating value for global command: '${globalCommand.name}' using potential args: ${(isSecret
27
+ ? potentialArgs
28
+ .slice(0, 1)
29
+ .map(() => REDACTED)
30
+ .concat(potentialArgs.slice(1))
31
+ : potentialArgs
32
+ ).join(" ")}`;
26
33
  if (defaultValue !== undefined) {
27
- return `${message} and configured value: '${defaultValue}'`;
34
+ return `${message} and configured value: '${isSecret ? REDACTED : defaultValue}'`;
28
35
  }
29
36
  return message;
30
37
  });
31
- const argument = globalCommand.argument!;
32
38
 
33
39
  // default to defined argument default value
34
40
  let populatedArgumentValue = argument.defaultValue;
@@ -83,5 +89,6 @@ export default function populateGlobalCommandValue(
83
89
  return {
84
90
  populatedArgumentValue,
85
91
  unusedArgs,
92
+ ...(isSecret && potentialArgs.length > 0 && { secretArgumentNames: [globalCommand.name] }),
86
93
  };
87
94
  }
@@ -8,6 +8,7 @@ import {
8
8
  type PopulatedValueType,
9
9
  } from "@flowscripter/dynamic-cli-framework-api";
10
10
  import getLogger from "../../util/logger.ts";
11
+ import { REDACTED, redactSubCommandValues } from "../../util/secretRedaction.ts";
11
12
  import argumentValueMerge from "./argumentValueMerge.ts";
12
13
  import type { SubCommandValuePopulationResult } from "./ValuePopulationResult.ts";
13
14
  import type { ComplexOption } from "@flowscripter/dynamic-cli-framework-api";
@@ -87,6 +88,20 @@ class ParseContext {
87
88
  */
88
89
  currentPositionalIndex;
89
90
 
91
+ /**
92
+ * Set when the most recent argument supplied a value for a SECRET option or positional.
93
+ */
94
+ secretArgument?: { name: string; inline: boolean };
95
+
96
+ /**
97
+ * Return and clear the {@link secretArgument} recorded by the most recent {@link parse}.
98
+ */
99
+ takeSecretArgument(): { name: string; inline: boolean } | undefined {
100
+ const secretArgument = this.secretArgument;
101
+ this.secretArgument = undefined;
102
+ return secretArgument;
103
+ }
104
+
90
105
  /**
91
106
  * Use to store lazily created map of option paths e.g. --alpha.b.g and -a.b.gamma for {@link ComplexOption} or {@link Option} instances.
92
107
  */
@@ -351,7 +366,10 @@ class ParseContext {
351
366
  * @return `false` if there is a problem populating the value, in which case the {@link state} will be set to
352
367
  * {@link ParseState.ERROR} and {@link invalidArgument} will be populated.
353
368
  */
354
- setOptionValue(value: SingleValueType): boolean {
369
+ setOptionValue(value: SingleValueType, inline = false): boolean {
370
+ if (this.currentOption!.type === ValueTypeName.SECRET) {
371
+ this.secretArgument = { name: this.currentOption!.name, inline };
372
+ }
355
373
  let optionValue: Array<PopulatedValues> | PopulatedValues | PopulatedValueType =
356
374
  this.populatedArgumentValues;
357
375
  const optionPath = this.currentOptionPath as Array<string | number>;
@@ -439,6 +457,10 @@ class ParseContext {
439
457
  const positional = this.subCommand.positionals[this.currentPositionalIndex]!;
440
458
  const currentValue = this.populatedArgumentValues[positional.name];
441
459
 
460
+ if (positional.type === ValueTypeName.SECRET) {
461
+ this.secretArgument = { name: positional.name, inline: false };
462
+ }
463
+
442
464
  // if we haven't populated the argument value before...
443
465
  if (currentValue === undefined) {
444
466
  if (positional.isVarargMultiple) {
@@ -546,7 +568,7 @@ class ParseContext {
546
568
  // try to set the value if we have one
547
569
  if (potentialOptionValue !== undefined) {
548
570
  // set the value
549
- this.setOptionValue(potentialOptionValue);
571
+ this.setOptionValue(potentialOptionValue, true);
550
572
 
551
573
  // return regardless
552
574
  return;
@@ -587,24 +609,23 @@ export default function populateSubCommandValues(
587
609
  potentialArgs: ReadonlyArray<string>,
588
610
  defaultValues: PopulatedValues | undefined,
589
611
  ): SubCommandValuePopulationResult {
590
- logger.debug(() => {
591
- const message = `Populating values for sub-command: '${subCommand.name}' using potential args: ${potentialArgs.join(
592
- " ",
593
- )}`;
594
- if (defaultValues !== undefined) {
595
- return `${message} and configured values: ${JSON.stringify(defaultValues)}`;
596
- }
597
- return message;
598
- });
599
-
600
612
  const parseContext = new ParseContext(subCommand);
601
613
  const unusedArgs: Array<string> = [];
614
+ const loggedArgs = [...potentialArgs];
615
+ const secretArgumentNames = new Set<string>();
602
616
 
603
617
  for (let i = 0; i < potentialArgs.length; i++) {
604
618
  const potentialArg = potentialArgs[i]!;
605
619
 
606
620
  // parse the next arg
607
621
  parseContext.parse(potentialArg);
622
+ const secretArgument = parseContext.takeSecretArgument();
623
+ if (secretArgument) {
624
+ secretArgumentNames.add(secretArgument.name);
625
+ loggedArgs[i] = secretArgument.inline
626
+ ? `${potentialArg.slice(0, potentialArg.indexOf("=") + 1)}${REDACTED}`
627
+ : REDACTED;
628
+ }
608
629
 
609
630
  // check if we had a parsing error, record the error, flush the remaining args and stop parsing
610
631
  if (parseContext.state === ParseState.ERROR) {
@@ -620,6 +641,18 @@ export default function populateSubCommandValues(
620
641
  }
621
642
  }
622
643
 
644
+ logger.debug(() => {
645
+ const message = `Populated values for sub-command: '${subCommand.name}' using potential args: ${loggedArgs.join(
646
+ " ",
647
+ )}`;
648
+ if (defaultValues !== undefined) {
649
+ return `${message} and configured values: ${JSON.stringify(
650
+ redactSubCommandValues(subCommand, defaultValues),
651
+ )}`;
652
+ }
653
+ return message;
654
+ });
655
+
623
656
  // Check if the last arg specified an option but no value
624
657
  if (parseContext.state === ParseState.OPTION_VALUE_EXPECTED) {
625
658
  // special case if the last arg was for a boolean option where the value was implicitly `true`
@@ -639,6 +672,7 @@ export default function populateSubCommandValues(
639
672
  populatedArgumentValues: parseContext.populatedArgumentValues,
640
673
  unusedArgs: unusedArgs,
641
674
  invalidArgument: parseContext.invalidArgument,
675
+ ...(secretArgumentNames.size > 0 && { secretArgumentNames: [...secretArgumentNames] }),
642
676
  };
643
677
  }
644
678
 
@@ -651,5 +685,6 @@ export default function populateSubCommandValues(
651
685
  return {
652
686
  populatedArgumentValues,
653
687
  unusedArgs: unusedArgs,
688
+ ...(secretArgumentNames.size > 0 && { secretArgumentNames: [...secretArgumentNames] }),
654
689
  };
655
690
  }
@@ -7,19 +7,22 @@ function isRemoteSession(): boolean {
7
7
  return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY);
8
8
  }
9
9
 
10
+ export function getOpenCommand(platform: string, url: string): string[] {
11
+ if (platform === "darwin") {
12
+ return ["open", url];
13
+ }
14
+ if (platform === "win32") {
15
+ return ["rundll32", "url.dll,FileProtocolHandler", url];
16
+ }
17
+ return ["xdg-open", url];
18
+ }
19
+
10
20
  export async function defaultOpenUrl(spawnService: SpawnService, url: string): Promise<void> {
11
21
  const parsed = new URL(url);
12
22
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
13
23
  throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
14
24
  }
15
- let cmd: string[];
16
- if (process.platform === "darwin") {
17
- cmd = ["open", url];
18
- } else if (process.platform === "win32") {
19
- cmd = ["cmd", "/c", "start", "", url];
20
- } else {
21
- cmd = ["xdg-open", url];
22
- }
25
+ const cmd = getOpenCommand(process.platform, parsed.href);
23
26
  const result = await spawnService.spawn(cmd, { mode: "ignore", longRunning: false });
24
27
  if (result.ok) {
25
28
  return;
@@ -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
+ }