@oclif/core 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.16.0](https://github.com/oclif/core/compare/v1.15.0...v1.16.0) (2022-08-24)
6
+
7
+
8
+ ### Features
9
+
10
+ * support complex flag relationships ([#468](https://github.com/oclif/core/issues/468)) ([222d1f6](https://github.com/oclif/core/commit/222d1f67012557ac0707077d6c8840966dbf00cb))
11
+
5
12
  ## [1.15.0](https://github.com/oclif/core/compare/v1.14.2...v1.15.0) (2022-08-23)
6
13
 
7
14
 
@@ -660,6 +660,7 @@ async function toCached(c, plugin) {
660
660
  multiple: flag.multiple,
661
661
  options: flag.options,
662
662
  dependsOn: flag.dependsOn,
663
+ relationships: flag.relationships,
663
664
  exclusive: flag.exclusive,
664
665
  default: await defaultToCached(flag),
665
666
  };
@@ -84,6 +84,14 @@ export declare type DefaultContext<T, P> = {
84
84
  };
85
85
  export declare type Default<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<T>);
86
86
  export declare type DefaultHelp<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<string | undefined>);
87
+ export declare type FlagRelationship = string | {
88
+ name: string;
89
+ when: (flags: Record<string, unknown>) => Promise<boolean>;
90
+ };
91
+ export declare type Relationship = {
92
+ type: 'all' | 'some' | 'none';
93
+ flags: FlagRelationship[];
94
+ };
87
95
  export declare type FlagProps = {
88
96
  name: string;
89
97
  char?: AlphabetLowercase | AlphabetUppercase;
@@ -131,6 +139,10 @@ export declare type FlagProps = {
131
139
  * Exactly one of these flags must be provided.
132
140
  */
133
141
  exactlyOne?: string[];
142
+ /**
143
+ * Define complex relationships between flags.
144
+ */
145
+ relationships?: Relationship[];
134
146
  };
135
147
  export declare type BooleanFlagProps = FlagProps & {
136
148
  type: 'boolean';
@@ -1,6 +1,12 @@
1
1
  import { CLIError } from '../errors';
2
2
  import { ParserArg, CLIParseErrorOptions, OptionFlag, Flag } from '../interfaces';
3
3
  export { CLIError } from '../errors';
4
+ export declare type Validation = {
5
+ name: string;
6
+ status: 'success' | 'failed';
7
+ validationFn: string;
8
+ reason?: string;
9
+ };
4
10
  export declare class CLIParseError extends CLIError {
5
11
  parse: CLIParseErrorOptions['parse'];
6
12
  constructor(options: CLIParseErrorOptions & {
@@ -37,3 +43,8 @@ export declare class FlagInvalidOptionError extends CLIParseError {
37
43
  export declare class ArgInvalidOptionError extends CLIParseError {
38
44
  constructor(arg: ParserArg<any>, input: string);
39
45
  }
46
+ export declare class FailedFlagValidationError extends CLIParseError {
47
+ constructor({ parse, failed }: CLIParseErrorOptions & {
48
+ failed: Validation[];
49
+ });
50
+ }
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ArgInvalidOptionError = exports.FlagInvalidOptionError = exports.UnexpectedArgsError = exports.RequiredFlagError = exports.RequiredArgsError = exports.InvalidArgsSpecError = exports.CLIParseError = exports.CLIError = void 0;
3
+ exports.FailedFlagValidationError = exports.ArgInvalidOptionError = exports.FlagInvalidOptionError = exports.UnexpectedArgsError = exports.RequiredFlagError = exports.RequiredArgsError = exports.InvalidArgsSpecError = exports.CLIParseError = exports.CLIError = void 0;
4
4
  const errors_1 = require("../errors");
5
5
  const deps_1 = require("./deps");
6
+ const util_1 = require("../config/util");
6
7
  var errors_2 = require("../errors");
7
8
  Object.defineProperty(exports, "CLIError", { enumerable: true, get: function () { return errors_2.CLIError; } });
8
9
  // eslint-disable-next-line new-cap
@@ -10,7 +11,8 @@ const m = (0, deps_1.default)()
10
11
  // eslint-disable-next-line node/no-missing-require
11
12
  .add('help', () => require('./help'))
12
13
  // eslint-disable-next-line node/no-missing-require
13
- .add('list', () => require('./list'));
14
+ .add('list', () => require('./list'))
15
+ .add('chalk', () => require('chalk'));
14
16
  class CLIParseError extends errors_1.CLIError {
15
17
  constructor(options) {
16
18
  options.message += '\nSee more help with --help';
@@ -76,3 +78,13 @@ class ArgInvalidOptionError extends CLIParseError {
76
78
  }
77
79
  }
78
80
  exports.ArgInvalidOptionError = ArgInvalidOptionError;
81
+ class FailedFlagValidationError extends CLIParseError {
82
+ constructor({ parse, failed }) {
83
+ const reasons = failed.map(r => r.reason);
84
+ const deduped = (0, util_1.uniq)(reasons);
85
+ const errString = deduped.length === 1 ? 'error' : 'errors';
86
+ const message = `The following ${errString} occurred:\n ${m.chalk.dim(deduped.join('\n '))}`;
87
+ super({ parse, message });
88
+ }
89
+ }
90
+ exports.FailedFlagValidationError = FailedFlagValidationError;
@@ -27,7 +27,7 @@ async function parse(argv, options) {
27
27
  };
28
28
  const parser = new parse_1.Parser(input);
29
29
  const output = await parser.parse();
30
- m.validate({ input, output });
30
+ await m.validate({ input, output });
31
31
  return output;
32
32
  }
33
33
  exports.parse = parse;
@@ -2,4 +2,4 @@ import { ParserInput, ParserOutput } from '../interfaces';
2
2
  export declare function validate(parse: {
3
3
  input: ParserInput;
4
4
  output: ParserOutput;
5
- }): void;
5
+ }): Promise<void>;
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validate = void 0;
4
- const errors_1 = require("../errors");
5
- const errors_2 = require("./errors");
6
- function validate(parse) {
4
+ const errors_1 = require("./errors");
5
+ const util_1 = require("../config/util");
6
+ async function validate(parse) {
7
7
  function validateArgs() {
8
8
  const maxArgs = parse.input.args.length;
9
9
  if (parse.input.strict && parse.output.argv.length > maxArgs) {
10
10
  const extras = parse.output.argv.slice(maxArgs);
11
- throw new errors_2.UnexpectedArgsError({ parse, args: extras });
11
+ throw new errors_1.UnexpectedArgsError({ parse, args: extras });
12
12
  }
13
13
  const missingRequiredArgs = [];
14
14
  let hasOptional = false;
@@ -19,63 +19,138 @@ function validate(parse) {
19
19
  else if (hasOptional) {
20
20
  // (required arg) check whether an optional has occurred before
21
21
  // optionals should follow required, not before
22
- throw new errors_2.InvalidArgsSpecError({ parse, args: parse.input.args });
22
+ throw new errors_1.InvalidArgsSpecError({ parse, args: parse.input.args });
23
23
  }
24
24
  if (arg.required && !parse.output.argv[index] && parse.output.argv[index] !== 0) {
25
25
  missingRequiredArgs.push(arg);
26
26
  }
27
27
  }
28
28
  if (missingRequiredArgs.length > 0) {
29
- throw new errors_2.RequiredArgsError({ parse, args: missingRequiredArgs });
29
+ throw new errors_1.RequiredArgsError({ parse, args: missingRequiredArgs });
30
30
  }
31
31
  }
32
+ async function validateFlags() {
33
+ const promises = Object.entries(parse.input.flags).map(async ([name, flag]) => {
34
+ const results = [];
35
+ if (parse.output.flags[name] !== undefined) {
36
+ results.push(...await validateRelationships(name, flag), await validateDependsOn(name, flag.dependsOn ?? []), await validateExclusive(name, flag.exclusive ?? []), await validateExactlyOne(name, flag.exactlyOne ?? []));
37
+ }
38
+ else if (flag.required) {
39
+ results.push({ status: 'failed', name, validationFn: 'required', reason: `Missing required flag ${name}` });
40
+ }
41
+ else if (flag.exactlyOne && flag.exactlyOne.length > 0) {
42
+ results.push(validateAcrossFlags(flag));
43
+ }
44
+ return results;
45
+ });
46
+ const results = (await Promise.all(promises)).flat();
47
+ const failed = results.filter(r => r.status === 'failed');
48
+ if (failed.length > 0)
49
+ throw new errors_1.FailedFlagValidationError({ parse, failed });
50
+ }
51
+ async function resolveFlags(flags) {
52
+ const promises = flags.map(async (flag) => {
53
+ if (typeof flag === 'string') {
54
+ return [flag, parse.output.flags[flag]];
55
+ }
56
+ const result = await flag.when(parse.output.flags);
57
+ return result ? [flag.name, parse.output.flags[flag.name]] : null;
58
+ });
59
+ const resolved = await Promise.all(promises);
60
+ return Object.fromEntries(resolved.filter(r => r !== null));
61
+ }
62
+ function getPresentFlags(flags) {
63
+ return Object.keys(flags).reduce((acc, key) => {
64
+ if (flags[key])
65
+ acc.push(key);
66
+ return acc;
67
+ }, []);
68
+ }
32
69
  function validateAcrossFlags(flag) {
70
+ const base = { name: flag.name, validationFn: 'validateAcrossFlags' };
33
71
  const intersection = Object.entries(parse.input.flags)
34
72
  .map(entry => entry[0]) // array of flag names
35
73
  .filter(flagName => parse.output.flags[flagName] !== undefined) // with values
36
74
  .filter(flagName => flag.exactlyOne && flag.exactlyOne.includes(flagName)); // and in the exactlyOne list
37
75
  if (intersection.length === 0) {
38
76
  // the command's exactlyOne may or may not include itself, so we'll use Set to add + de-dupe
39
- throw new errors_1.CLIError(`Exactly one of the following must be provided: ${[
40
- ...new Set(flag.exactlyOne?.map(flag => `--${flag}`)),
41
- ].join(', ')}`);
77
+ const deduped = (0, util_1.uniq)(flag.exactlyOne?.map(flag => `--${flag}`) ?? []).join(', ');
78
+ const reason = `Exactly one of the following must be provided: ${deduped}`;
79
+ return { ...base, status: 'failed', reason };
42
80
  }
81
+ return { ...base, status: 'success' };
43
82
  }
44
- function validateFlags() {
45
- for (const [name, flag] of Object.entries(parse.input.flags)) {
46
- if (parse.output.flags[name] !== undefined) {
47
- for (const also of flag.dependsOn || []) {
48
- if (!parse.output.flags[also]) {
49
- throw new errors_1.CLIError(`--${also}= must also be provided when using --${name}=`);
50
- }
51
- }
52
- for (const also of flag.exclusive || []) {
53
- // do not enforce exclusivity for flags that were defaulted
54
- if (parse.output.metadata.flags[also] &&
55
- parse.output.metadata.flags[also].setFromDefault)
56
- continue;
57
- if (parse.output.metadata.flags[name] &&
58
- parse.output.metadata.flags[name].setFromDefault)
59
- continue;
60
- if (parse.output.flags[also]) {
61
- throw new errors_1.CLIError(`--${also}= cannot also be provided when using --${name}=`);
62
- }
63
- }
64
- for (const also of flag.exactlyOne || []) {
65
- if (also !== name && parse.output.flags[also]) {
66
- throw new errors_1.CLIError(`--${also}= cannot also be provided when using --${name}=`);
67
- }
68
- }
83
+ async function validateExclusive(name, flags) {
84
+ const base = { name, validationFn: 'validateExclusive' };
85
+ const resolved = await resolveFlags(flags);
86
+ const keys = getPresentFlags(resolved);
87
+ for (const flag of keys) {
88
+ // do not enforce exclusivity for flags that were defaulted
89
+ if (parse.output.metadata.flags && parse.output.metadata.flags[flag]?.setFromDefault)
90
+ continue;
91
+ if (parse.output.metadata.flags && parse.output.metadata.flags[name]?.setFromDefault)
92
+ continue;
93
+ if (parse.output.flags[flag]) {
94
+ return { ...base, status: 'failed', reason: `--${flag}=${parse.output.flags[flag]} cannot also be provided when using --${name}` };
69
95
  }
70
- else if (flag.required) {
71
- throw new errors_2.RequiredFlagError({ parse, flag });
72
- }
73
- else if (flag.exactlyOne && flag.exactlyOne.length > 0) {
74
- validateAcrossFlags(flag);
96
+ }
97
+ return { ...base, status: 'success' };
98
+ }
99
+ async function validateExactlyOne(name, flags) {
100
+ const base = { name, validationFn: 'validateExactlyOne' };
101
+ const resolved = await resolveFlags(flags);
102
+ const keys = getPresentFlags(resolved);
103
+ for (const flag of keys) {
104
+ if (flag !== name && parse.output.flags[flag]) {
105
+ return { ...base, status: 'failed', reason: `--${flag} cannot also be provided when using --${name}` };
75
106
  }
76
107
  }
108
+ return { ...base, status: 'success' };
109
+ }
110
+ async function validateDependsOn(name, flags) {
111
+ const base = { name, validationFn: 'validateDependsOn' };
112
+ const resolved = await resolveFlags(flags);
113
+ const foundAll = Object.values(resolved).every(Boolean);
114
+ if (!foundAll) {
115
+ const formattedFlags = Object.keys(resolved).map(f => `--${f}`).join(', ');
116
+ return { ...base, status: 'failed', reason: `All of the following must be provided when using --${name}: ${formattedFlags}` };
117
+ }
118
+ return { ...base, status: 'success' };
119
+ }
120
+ async function validateSome(name, flags) {
121
+ const base = { name, validationFn: 'validateSome' };
122
+ const resolved = await resolveFlags(flags);
123
+ const foundAtLeastOne = Object.values(resolved).some(Boolean);
124
+ if (!foundAtLeastOne) {
125
+ const formattedFlags = Object.keys(resolved).map(f => `--${f}`).join(', ');
126
+ return { ...base, status: 'failed', reason: `One of the following must be provided when using --${name}: ${formattedFlags}` };
127
+ }
128
+ return { ...base, status: 'success' };
129
+ }
130
+ async function validateRelationships(name, flag) {
131
+ if (!flag.relationships)
132
+ return [];
133
+ const results = await Promise.all(flag.relationships.map(async (relationship) => {
134
+ const flags = relationship.flags ?? [];
135
+ const results = [];
136
+ switch (relationship.type) {
137
+ case 'all':
138
+ results.push(await validateDependsOn(name, flags));
139
+ break;
140
+ case 'some':
141
+ results.push(await validateSome(name, flags));
142
+ break;
143
+ case 'none':
144
+ results.push(await validateExclusive(name, flags));
145
+ break;
146
+ default:
147
+ break;
148
+ }
149
+ return results;
150
+ }));
151
+ return results.flat();
77
152
  }
78
153
  validateArgs();
79
- validateFlags();
154
+ await validateFlags();
80
155
  }
81
156
  exports.validate = validate;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/core",
3
3
  "description": "base library for oclif CLIs",
4
- "version": "1.15.0",
4
+ "version": "1.16.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/core/issues",
7
7
  "dependencies": {