@oclif/core 1.5.2 → 1.6.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
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.6.1](https://github.com/oclif/core/compare/v1.6.0...v1.6.1) (2022-03-17)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * set id to alias when adding commands ([#390](https://github.com/oclif/core/issues/390)) ([84ab722](https://github.com/oclif/core/commit/84ab7223a2196c6a33f64a3e4ba75a050b02d1c3))
11
+
12
+ ## [1.6.0](https://github.com/oclif/core/compare/v1.5.3...v1.6.0) (2022-03-14)
13
+
14
+
15
+ ### Features
16
+
17
+ * POC for allowing flexible command taxonomy ([#376](https://github.com/oclif/core/issues/376)) ([c47c6c6](https://github.com/oclif/core/commit/c47c6c6fb689a92f66d40aacfa146d885f08d962))
18
+
19
+ ### [1.5.3](https://github.com/oclif/core/compare/v1.5.2...v1.5.3) (2022-03-09)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * rid core of transient refs to cli-ux ([#379](https://github.com/oclif/core/issues/379)) ([a593a27](https://github.com/oclif/core/commit/a593a2751dbdd4bcd9cf05349154d0fa6e4d7e2d))
25
+
5
26
  ### [1.5.2](https://github.com/oclif/core/compare/v1.5.1...v1.5.2) (2022-03-04)
6
27
 
7
28
 
@@ -29,10 +29,13 @@ export declare class Config implements IConfig {
29
29
  binPath?: string;
30
30
  valid: boolean;
31
31
  topicSeparator: ':' | ' ';
32
+ flexibleTaxonomy: boolean;
32
33
  protected warned: boolean;
33
- private _commands?;
34
- private _commandIDs?;
35
- private _topics?;
34
+ private commandPermutations;
35
+ private topicPermutations;
36
+ private _commands;
37
+ private _topics;
38
+ private _commandIDs;
36
39
  constructor(options: Options);
37
40
  static load(opts?: LoadOptions): Promise<IConfig | Config>;
38
41
  load(): Promise<void>;
@@ -56,6 +59,30 @@ export declare class Config implements IConfig {
56
59
  findTopic(id: string, opts?: {
57
60
  must: boolean;
58
61
  }): Topic | undefined;
62
+ /**
63
+ * Find all command ids that include the provided command id.
64
+ *
65
+ * For example, if the command ids are:
66
+ * - foo:bar:baz
67
+ * - one:two:three
68
+ *
69
+ * `bar` would return `foo:bar:baz`
70
+ *
71
+ * @param partialCmdId string
72
+ * @param argv string[] process.argv containing the flags and arguments provided by the user
73
+ * @returns string[]
74
+ */
75
+ findMatches(partialCmdId: string, argv: string[]): Command.Plugin[];
76
+ /**
77
+ * Returns an array of all commands. If flexible taxonomy is enabled then all permutations will be appended to the array.
78
+ * @returns Command.Plugin[]
79
+ */
80
+ getAllCommands(): Command.Plugin[];
81
+ /**
82
+ * Returns an array of all command ids. If flexible taxonomy is enabled then all permutations will be appended to the array.
83
+ * @returns string[]
84
+ */
85
+ getAllCommandIDs(): string[];
59
86
  get commands(): Command.Plugin[];
60
87
  get commandIDs(): string[];
61
88
  get topics(): Topic[];
@@ -78,5 +105,29 @@ export declare class Config implements IConfig {
78
105
  detail: string;
79
106
  }, scope?: string): void;
80
107
  protected get isProd(): boolean;
108
+ private getCmdLookupId;
109
+ private getTopicLookupId;
110
+ private loadCommands;
111
+ private loadTopics;
112
+ /**
113
+ * This method is responsible for locating the correct plugin to use for a named command id
114
+ * It searches the {Config} registered commands to match either the raw command id or the command alias
115
+ * It is possible that more than one command will be found. This is due the ability of two distinct plugins to
116
+ * create the same command or command alias.
117
+ *
118
+ * In the case of more than one found command, the function will select the command based on the order in which
119
+ * the plugin is included in the package.json `oclif.plugins` list. The command that occurs first in the list
120
+ * is selected as the command to run.
121
+ *
122
+ * Commands can also be present from either an install or a link. When a command is one of these and a core plugin
123
+ * is present, this function defers to the core plugin.
124
+ *
125
+ * If there is not a core plugin command present, this function will return the first
126
+ * plugin as discovered (will not change the order)
127
+ *
128
+ * @param commands commands to determine the priority of
129
+ * @returns command instance {Command.Plugin} or undefined
130
+ */
131
+ private determinePriority;
81
132
  }
82
133
  export declare function toCached(c: Command.Class, plugin?: IPlugin): Promise<Command>;
@@ -7,11 +7,11 @@ const os = require("os");
7
7
  const path = require("path");
8
8
  const url_1 = require("url");
9
9
  const util_1 = require("util");
10
- const util_2 = require("./util");
11
10
  const Plugin = require("./plugin");
12
- const util_3 = require("./util");
13
- const util_4 = require("../util");
11
+ const util_2 = require("./util");
12
+ const util_3 = require("../util");
14
13
  const module_loader_1 = require("../module-loader");
14
+ const util_4 = require("../help/util");
15
15
  // eslint-disable-next-line new-cap
16
16
  const debug = (0, util_2.Debug)();
17
17
  const _pjson = require('../../package.json');
@@ -23,6 +23,36 @@ const WSL = require('is-wsl');
23
23
  function isConfig(o) {
24
24
  return o && Boolean(o._base);
25
25
  }
26
+ class Permutations extends Map {
27
+ constructor() {
28
+ super(...arguments);
29
+ this.validPermutations = new Map();
30
+ }
31
+ add(permutation, commandId) {
32
+ this.validPermutations.set(permutation, commandId);
33
+ for (const id of (0, util_2.collectUsableIds)([permutation])) {
34
+ if (this.has(id)) {
35
+ this.set(id, this.get(id).add(commandId));
36
+ }
37
+ else {
38
+ this.set(id, new Set([commandId]));
39
+ }
40
+ }
41
+ }
42
+ get(key) {
43
+ var _a;
44
+ return (_a = super.get(key)) !== null && _a !== void 0 ? _a : new Set();
45
+ }
46
+ getValid(key) {
47
+ return this.validPermutations.get(key);
48
+ }
49
+ getAllValid() {
50
+ return [...this.validPermutations.keys()];
51
+ }
52
+ hasValid(key) {
53
+ return this.validPermutations.has(key);
54
+ }
55
+ }
26
56
  class Config {
27
57
  // eslint-disable-next-line no-useless-constructor
28
58
  constructor(options) {
@@ -32,6 +62,10 @@ class Config {
32
62
  this.plugins = [];
33
63
  this.topicSeparator = ':';
34
64
  this.warned = false;
65
+ this.commandPermutations = new Permutations();
66
+ this.topicPermutations = new Permutations();
67
+ this._commands = new Map();
68
+ this._topics = new Map();
35
69
  }
36
70
  static async load(opts = (module.parent && module.parent.parent && module.parent.parent.filename) || __dirname) {
37
71
  // Handle the case when a file URL string is passed in such as 'import.meta.url'; covert to file path.
@@ -62,6 +96,7 @@ class Config {
62
96
  this.windows = this.platform === 'win32';
63
97
  this.bin = this.pjson.oclif.bin || this.name;
64
98
  this.dirname = this.pjson.oclif.dirname || this.name;
99
+ this.flexibleTaxonomy = this.pjson.oclif.flexibleTaxonomy || false;
65
100
  // currently, only colons or spaces are valid separators
66
101
  if (this.pjson.oclif.topicSeparator && [':', ' '].includes(this.pjson.oclif.topicSeparator))
67
102
  this.topicSeparator = this.pjson.oclif.topicSeparator;
@@ -104,6 +139,10 @@ class Config {
104
139
  await this.loadUserPlugins();
105
140
  await this.loadDevPlugins();
106
141
  await this.loadCorePlugins();
142
+ for (const plugin of this.plugins) {
143
+ this.loadCommands(plugin);
144
+ this.loadTopics(plugin);
145
+ }
107
146
  debug('config done');
108
147
  }
109
148
  async loadCorePlugins() {
@@ -129,7 +168,7 @@ class Config {
129
168
  try {
130
169
  const userPJSONPath = path.join(this.dataDir, 'package.json');
131
170
  debug('reading user plugins pjson %s', userPJSONPath);
132
- const pjson = await (0, util_3.loadJSON)(userPJSONPath);
171
+ const pjson = await (0, util_2.loadJSON)(userPJSONPath);
133
172
  this.userPJSON = pjson;
134
173
  if (!pjson.oclif)
135
174
  pjson.oclif = { schema: 1 };
@@ -214,7 +253,10 @@ class Config {
214
253
  debug('runCommand %s %o', id, argv);
215
254
  const c = cachedCommand || this.findCommand(id);
216
255
  if (!c) {
217
- const hookResult = await this.runHook('command_not_found', { id, argv });
256
+ const matches = this.flexibleTaxonomy ? this.findMatches(id, argv) : [];
257
+ const hookResult = this.flexibleTaxonomy && matches.length > 0 ?
258
+ await this.runHook('command_incomplete', { id, argv, matches }) :
259
+ await this.runHook('command_not_found', { id, argv });
218
260
  if (hookResult.successes[0]) {
219
261
  const cmdResult = hookResult.successes[0].result;
220
262
  return cmdResult;
@@ -240,106 +282,79 @@ class Config {
240
282
  .join('_')
241
283
  .toUpperCase();
242
284
  }
243
- /**
244
- * This function is responsible for locating the correct plugin to use for a named command id
245
- * It searches the {Config} registered commands to match either the raw command id or the command alias
246
- * It is possible that more than one command will be found. This is due the ability of two distinct plugins to
247
- * create the same command or command alias.
248
- *
249
- * In the case of more than one found command, the function will select the command based on the order in which
250
- * the plugin is included in the package.json `oclif.plugins` list. The command that occurs first in the list
251
- * is selected as the command to run.
252
- *
253
- * Commands can also be present from either an install or a link. When a command is one of these and a core plugin
254
- * is present, this function defers to the core plugin.
255
- *
256
- * If there is not a core plugin command present, this function will return the first
257
- * plugin as discovered (will not change the order)
258
- * @param id raw command id or command alias
259
- * @param opts options to control if the command must be found
260
- * @returns command instance {Command.Plugin} or undefined
261
- */
262
285
  findCommand(id, opts = {}) {
263
- var _a, _b;
264
- const commands = this.commands.filter(c => c.id === id || c.aliases.includes(id));
265
- if (opts.must && commands.length === 0)
266
- (0, errors_1.error)(`command ${id} not found`);
267
- if (commands.length === 1)
268
- return commands[0];
269
- // more than one command found across available plugins
270
- const oclifPlugins = (_b = (_a = this.pjson.oclif) === null || _a === void 0 ? void 0 : _a.plugins) !== null && _b !== void 0 ? _b : [];
271
- const commandPlugins = commands.sort((a, b) => {
272
- var _a, _b;
273
- const pluginAliasA = (_a = a.pluginAlias) !== null && _a !== void 0 ? _a : 'A-Cannot-Find-This';
274
- const pluginAliasB = (_b = b.pluginAlias) !== null && _b !== void 0 ? _b : 'B-Cannot-Find-This';
275
- const aIndex = oclifPlugins.indexOf(pluginAliasA);
276
- const bIndex = oclifPlugins.indexOf(pluginAliasB);
277
- // When both plugin types are 'core' plugins sort based on index
278
- if (a.pluginType === 'core' && b.pluginType === 'core') {
279
- // If b appears first in the pjson.plugins sort it first
280
- return aIndex - bIndex;
281
- }
282
- // if b is a core plugin and a is not sort b first
283
- if (b.pluginType === 'core' && a.pluginType !== 'core') {
284
- return 1;
285
- }
286
- // if a is a core plugin and b is not sort a first
287
- if (a.pluginType === 'core' && b.pluginType !== 'core') {
288
- return -1;
289
- }
290
- // neither plugin is core, so do not change the order
291
- return 0;
292
- });
293
- return commandPlugins[0];
286
+ const lookupId = this.getCmdLookupId(id);
287
+ const command = this._commands.get(lookupId);
288
+ if (opts.must && !command)
289
+ (0, errors_1.error)(`command ${lookupId} not found`);
290
+ return command;
294
291
  }
295
292
  findTopic(name, opts = {}) {
296
- const topic = this.topics.find(t => t.name === name);
293
+ const lookupId = this.getTopicLookupId(name);
294
+ const topic = this._topics.get(lookupId);
297
295
  if (topic)
298
296
  return topic;
299
297
  if (opts.must)
300
298
  throw new Error(`topic ${name} not found`);
301
299
  }
300
+ /**
301
+ * Find all command ids that include the provided command id.
302
+ *
303
+ * For example, if the command ids are:
304
+ * - foo:bar:baz
305
+ * - one:two:three
306
+ *
307
+ * `bar` would return `foo:bar:baz`
308
+ *
309
+ * @param partialCmdId string
310
+ * @param argv string[] process.argv containing the flags and arguments provided by the user
311
+ * @returns string[]
312
+ */
313
+ findMatches(partialCmdId, argv) {
314
+ const flags = argv.filter(arg => !(0, util_4.getHelpFlagAdditions)(this).includes(arg) && arg.startsWith('-')).map(a => a.replace(/-/g, ''));
315
+ const possibleMatches = [...this.commandPermutations.get(partialCmdId)].map(k => this._commands.get(k));
316
+ const matches = possibleMatches.filter(command => {
317
+ const cmdFlags = Object.entries(command.flags).flatMap(([flag, def]) => {
318
+ return def.char ? [def.char, flag] : [flag];
319
+ });
320
+ // A command is a match if the provided flags belong to the full command
321
+ return flags.every(f => cmdFlags.includes(f));
322
+ });
323
+ return matches;
324
+ }
325
+ /**
326
+ * Returns an array of all commands. If flexible taxonomy is enabled then all permutations will be appended to the array.
327
+ * @returns Command.Plugin[]
328
+ */
329
+ getAllCommands() {
330
+ const commands = [...this._commands.values()];
331
+ const validPermutations = [...this.commandPermutations.getAllValid()];
332
+ for (const permutation of validPermutations) {
333
+ if (!this._commands.has(permutation)) {
334
+ const cmd = this._commands.get(this.getCmdLookupId(permutation));
335
+ commands.push({ ...cmd, id: permutation });
336
+ }
337
+ }
338
+ return commands;
339
+ }
340
+ /**
341
+ * Returns an array of all command ids. If flexible taxonomy is enabled then all permutations will be appended to the array.
342
+ * @returns string[]
343
+ */
344
+ getAllCommandIDs() {
345
+ return this.getAllCommands().map(c => c.id);
346
+ }
302
347
  get commands() {
303
- if (this._commands)
304
- return this._commands;
305
- this._commands = (0, util_3.flatMap)(this.plugins, p => p.commands);
306
- return this._commands;
348
+ return [...this._commands.values()];
307
349
  }
308
350
  get commandIDs() {
309
351
  if (this._commandIDs)
310
352
  return this._commandIDs;
311
- const ids = this.commands.flatMap(c => [c.id, ...c.aliases]);
312
- this._commandIDs = (0, util_3.uniq)(ids);
353
+ this._commandIDs = this.commands.map(c => c.id);
313
354
  return this._commandIDs;
314
355
  }
315
356
  get topics() {
316
- if (this._topics)
317
- return this._topics;
318
- const topics = [];
319
- for (const plugin of this.plugins) {
320
- for (const topic of (0, util_3.compact)(plugin.topics)) {
321
- const existing = topics.find(t => t.name === topic.name);
322
- if (existing) {
323
- existing.description = topic.description || existing.description;
324
- existing.hidden = existing.hidden || topic.hidden;
325
- }
326
- else
327
- topics.push(topic);
328
- }
329
- }
330
- // add missing topics
331
- for (const c of this.commands.filter(c => !c.hidden)) {
332
- const parts = c.id.split(':');
333
- while (parts.length > 0) {
334
- const name = parts.join(':');
335
- if (name && !topics.find(t => t.name === name)) {
336
- topics.push({ name, description: c.summary || c.description });
337
- }
338
- parts.pop();
339
- }
340
- }
341
- this._topics = topics;
342
- return this._topics;
357
+ return [...this._topics.values()];
343
358
  }
344
359
  s3Key(type, ext, options = {}) {
345
360
  var _a;
@@ -444,7 +459,7 @@ class Config {
444
459
  if (err instanceof Error) {
445
460
  const modifiedErr = err;
446
461
  modifiedErr.name = `${err.name} Plugin: ${this.name}`;
447
- modifiedErr.detail = (0, util_3.compact)([
462
+ modifiedErr.detail = (0, util_2.compact)([
448
463
  err.detail,
449
464
  `module: ${this._base}`,
450
465
  scope && `task: ${scope}`,
@@ -458,7 +473,7 @@ class Config {
458
473
  // err is an object
459
474
  process.emitWarning('Config.warn expected either a string or Error, but instead received an object');
460
475
  err.name = `${err.name} Plugin: ${this.name}`;
461
- err.detail = (0, util_3.compact)([
476
+ err.detail = (0, util_2.compact)([
462
477
  err.detail,
463
478
  `module: ${this._base}`,
464
479
  scope && `task: ${scope}`,
@@ -469,7 +484,123 @@ class Config {
469
484
  process.emitWarning(JSON.stringify(err));
470
485
  }
471
486
  get isProd() {
472
- return (0, util_4.isProd)();
487
+ return (0, util_3.isProd)();
488
+ }
489
+ getCmdLookupId(id) {
490
+ if (this._commands.has(id))
491
+ return id;
492
+ if (this.commandPermutations.hasValid(id))
493
+ return this.commandPermutations.getValid(id);
494
+ return id;
495
+ }
496
+ getTopicLookupId(id) {
497
+ if (this._topics.has(id))
498
+ return id;
499
+ if (this.topicPermutations.hasValid(id))
500
+ return this.topicPermutations.getValid(id);
501
+ return id;
502
+ }
503
+ loadCommands(plugin) {
504
+ var _a;
505
+ for (const command of plugin.commands) {
506
+ if (this._commands.has(command.id)) {
507
+ const prioritizedCommand = this.determinePriority([this._commands.get(command.id), command]);
508
+ this._commands.set(prioritizedCommand.id, prioritizedCommand);
509
+ }
510
+ else {
511
+ this._commands.set(command.id, command);
512
+ }
513
+ const permutations = this.flexibleTaxonomy ? (0, util_2.getCommandIdPermutations)(command.id) : [command.id];
514
+ for (const permutation of permutations) {
515
+ this.commandPermutations.add(permutation, command.id);
516
+ }
517
+ for (const alias of (_a = command.aliases) !== null && _a !== void 0 ? _a : []) {
518
+ if (this._commands.has(alias)) {
519
+ const prioritizedCommand = this.determinePriority([this._commands.get(alias), command]);
520
+ this._commands.set(prioritizedCommand.id, { ...prioritizedCommand, id: alias });
521
+ }
522
+ else {
523
+ this._commands.set(alias, { ...command, id: alias });
524
+ }
525
+ const aliasPermutations = this.flexibleTaxonomy ? (0, util_2.getCommandIdPermutations)(alias) : [alias];
526
+ for (const permutation of aliasPermutations) {
527
+ this.commandPermutations.add(permutation, command.id);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ loadTopics(plugin) {
533
+ for (const topic of (0, util_2.compact)(plugin.topics)) {
534
+ const existing = this._topics.get(topic.name);
535
+ if (existing) {
536
+ existing.description = topic.description || existing.description;
537
+ existing.hidden = existing.hidden || topic.hidden;
538
+ }
539
+ else {
540
+ this._topics.set(topic.name, topic);
541
+ }
542
+ const permutations = this.flexibleTaxonomy ? (0, util_2.getCommandIdPermutations)(topic.name) : [topic.name];
543
+ for (const permutation of permutations) {
544
+ this.topicPermutations.add(permutation, topic.name);
545
+ }
546
+ }
547
+ // Add missing topics for displaying help when partial commands are entered.
548
+ for (const c of plugin.commands.filter(c => !c.hidden)) {
549
+ const parts = c.id.split(':');
550
+ while (parts.length > 0) {
551
+ const name = parts.join(':');
552
+ if (name && !this._topics.has(name)) {
553
+ this._topics.set(name, { name, description: c.summary || c.description });
554
+ }
555
+ parts.pop();
556
+ }
557
+ }
558
+ }
559
+ /**
560
+ * This method is responsible for locating the correct plugin to use for a named command id
561
+ * It searches the {Config} registered commands to match either the raw command id or the command alias
562
+ * It is possible that more than one command will be found. This is due the ability of two distinct plugins to
563
+ * create the same command or command alias.
564
+ *
565
+ * In the case of more than one found command, the function will select the command based on the order in which
566
+ * the plugin is included in the package.json `oclif.plugins` list. The command that occurs first in the list
567
+ * is selected as the command to run.
568
+ *
569
+ * Commands can also be present from either an install or a link. When a command is one of these and a core plugin
570
+ * is present, this function defers to the core plugin.
571
+ *
572
+ * If there is not a core plugin command present, this function will return the first
573
+ * plugin as discovered (will not change the order)
574
+ *
575
+ * @param commands commands to determine the priority of
576
+ * @returns command instance {Command.Plugin} or undefined
577
+ */
578
+ determinePriority(commands) {
579
+ var _a, _b;
580
+ const oclifPlugins = (_b = (_a = this.pjson.oclif) === null || _a === void 0 ? void 0 : _a.plugins) !== null && _b !== void 0 ? _b : [];
581
+ const commandPlugins = commands.sort((a, b) => {
582
+ var _a, _b;
583
+ const pluginAliasA = (_a = a.pluginAlias) !== null && _a !== void 0 ? _a : 'A-Cannot-Find-This';
584
+ const pluginAliasB = (_b = b.pluginAlias) !== null && _b !== void 0 ? _b : 'B-Cannot-Find-This';
585
+ const aIndex = oclifPlugins.indexOf(pluginAliasA);
586
+ const bIndex = oclifPlugins.indexOf(pluginAliasB);
587
+ // When both plugin types are 'core' plugins sort based on index
588
+ if (a.pluginType === 'core' && b.pluginType === 'core') {
589
+ // If b appears first in the pjson.plugins sort it first
590
+ return aIndex - bIndex;
591
+ }
592
+ // if b is a core plugin and a is not sort b first
593
+ if (b.pluginType === 'core' && a.pluginType !== 'core') {
594
+ return 1;
595
+ }
596
+ // if a is a core plugin and b is not sort a first
597
+ if (a.pluginType === 'core' && b.pluginType !== 'core') {
598
+ return -1;
599
+ }
600
+ // neither plugin is core, so do not change the order
601
+ return 0;
602
+ });
603
+ return commandPlugins[0];
473
604
  }
474
605
  }
475
606
  exports.Config = Config;
@@ -126,9 +126,10 @@ class Plugin {
126
126
  }
127
127
  this.hooks = (0, util_3.mapValues)(this.pjson.oclif.hooks || {}, i => Array.isArray(i) ? i : [i]);
128
128
  this.manifest = await this._manifest(Boolean(this.options.ignoreManifest), Boolean(this.options.errorOnManifestCreate));
129
- this.commands = Object.entries(this.manifest.commands)
130
- .map(([id, c]) => ({ ...c, pluginAlias: this.alias, pluginType: this.type, load: async () => this.findCommand(id, { must: true }) }));
131
- this.commands.sort((a, b) => a.id.localeCompare(b.id));
129
+ this.commands = Object
130
+ .entries(this.manifest.commands)
131
+ .map(([id, c]) => ({ ...c, pluginAlias: this.alias, pluginType: this.type, load: async () => this.findCommand(id, { must: true }) }))
132
+ .sort((a, b) => a.id.localeCompare(b.id));
132
133
  }
133
134
  get topics() {
134
135
  return topicsToArray(this.pjson.oclif.topics || {});
@@ -12,3 +12,25 @@ export declare function loadJSON(path: string): Promise<any>;
12
12
  export declare function compact<T>(a: (T | undefined)[]): T[];
13
13
  export declare function uniq<T>(arr: T[]): T[];
14
14
  export declare function Debug(...scope: string[]): (..._: any) => void;
15
+ export declare function getPermutations(arr: string[]): Array<string[]>;
16
+ export declare function getCommandIdPermutations(commandId: string): string[];
17
+ /**
18
+ * Return an array of ids that represent all the usable combinations that a user could enter.
19
+ *
20
+ * For example, if the command ids are:
21
+ * - foo:bar:baz
22
+ * - one:two:three
23
+ * Then the usable ids would be:
24
+ * - foo
25
+ * - foo:bar
26
+ * - foo:bar:baz
27
+ * - one
28
+ * - one:two
29
+ * - one:two:three
30
+ *
31
+ * This allows us to determine which parts of the argv array belong to the command id whenever the topicSeparator is a space.
32
+ *
33
+ * @param commandIds string[]
34
+ * @returns string[]
35
+ */
36
+ export declare function collectUsableIds(commandIds: string[]): string[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Debug = exports.uniq = exports.compact = exports.loadJSON = exports.resolvePackage = exports.exists = exports.mapValues = exports.flatMap = void 0;
3
+ exports.collectUsableIds = exports.getCommandIdPermutations = exports.getPermutations = exports.Debug = exports.uniq = exports.compact = exports.loadJSON = exports.resolvePackage = exports.exists = exports.mapValues = exports.flatMap = void 0;
4
4
  const fs = require("fs");
5
5
  const debug = require('debug');
6
6
  function flatMap(arr, fn) {
@@ -46,9 +46,7 @@ function compact(a) {
46
46
  }
47
47
  exports.compact = compact;
48
48
  function uniq(arr) {
49
- return arr.filter((a, i) => {
50
- return !arr.find((b, j) => j > i && b === a);
51
- });
49
+ return [...new Set(arr)].sort();
52
50
  }
53
51
  exports.uniq = uniq;
54
52
  function displayWarnings() {
@@ -69,3 +67,61 @@ function Debug(...scope) {
69
67
  return (...args) => d(...args);
70
68
  }
71
69
  exports.Debug = Debug;
70
+ // Adapted from https://github.com/angus-c/just/blob/master/packages/array-permutations/index.js
71
+ function getPermutations(arr) {
72
+ if (arr.length === 0)
73
+ return [];
74
+ if (arr.length === 1)
75
+ return [arr];
76
+ const output = [];
77
+ const partialPermutations = getPermutations(arr.slice(1));
78
+ const first = arr[0];
79
+ for (let i = 0, len = partialPermutations.length; i < len; i++) {
80
+ const partial = partialPermutations[i];
81
+ for (let j = 0, len2 = partial.length; j <= len2; j++) {
82
+ const start = partial.slice(0, j);
83
+ const end = partial.slice(j);
84
+ const merged = start.concat(first, end);
85
+ output.push(merged);
86
+ }
87
+ }
88
+ return output;
89
+ }
90
+ exports.getPermutations = getPermutations;
91
+ function getCommandIdPermutations(commandId) {
92
+ return getPermutations(commandId.split(':')).flatMap(c => c.join(':'));
93
+ }
94
+ exports.getCommandIdPermutations = getCommandIdPermutations;
95
+ /**
96
+ * Return an array of ids that represent all the usable combinations that a user could enter.
97
+ *
98
+ * For example, if the command ids are:
99
+ * - foo:bar:baz
100
+ * - one:two:three
101
+ * Then the usable ids would be:
102
+ * - foo
103
+ * - foo:bar
104
+ * - foo:bar:baz
105
+ * - one
106
+ * - one:two
107
+ * - one:two:three
108
+ *
109
+ * This allows us to determine which parts of the argv array belong to the command id whenever the topicSeparator is a space.
110
+ *
111
+ * @param commandIds string[]
112
+ * @returns string[]
113
+ */
114
+ function collectUsableIds(commandIds) {
115
+ const usuableIds = [];
116
+ for (const id of commandIds) {
117
+ const parts = id.split(':');
118
+ while (parts.length > 0) {
119
+ const name = parts.join(':');
120
+ if (name)
121
+ usuableIds.push(name);
122
+ parts.pop();
123
+ }
124
+ }
125
+ return uniq(usuableIds).sort();
126
+ }
127
+ exports.collectUsableIds = collectUsableIds;
@@ -2,8 +2,7 @@ import * as Interfaces from '../interfaces';
2
2
  import CommandHelp from './command';
3
3
  import { HelpFormatter } from './formatter';
4
4
  export { CommandHelp } from './command';
5
- export { standardizeIDFromArgv, loadHelpClass } from './util';
6
- export declare function getHelpFlagAdditions(config: Interfaces.Config): string[];
5
+ export { standardizeIDFromArgv, loadHelpClass, getHelpFlagAdditions } from './util';
7
6
  export declare abstract class HelpBase extends HelpFormatter {
8
7
  constructor(config: Interfaces.Config, opts?: Partial<Interfaces.HelpOptions>);
9
8
  /**
package/lib/help/index.js CHANGED
@@ -15,16 +15,10 @@ Object.defineProperty(exports, "CommandHelp", { enumerable: true, get: function
15
15
  var util_3 = require("./util");
16
16
  Object.defineProperty(exports, "standardizeIDFromArgv", { enumerable: true, get: function () { return util_3.standardizeIDFromArgv; } });
17
17
  Object.defineProperty(exports, "loadHelpClass", { enumerable: true, get: function () { return util_3.loadHelpClass; } });
18
- const helpFlags = ['--help'];
19
- function getHelpFlagAdditions(config) {
20
- var _a;
21
- const additionalHelpFlags = (_a = config.pjson.oclif.additionalHelpFlags) !== null && _a !== void 0 ? _a : [];
22
- return [...new Set([...helpFlags, ...additionalHelpFlags]).values()];
23
- }
24
- exports.getHelpFlagAdditions = getHelpFlagAdditions;
18
+ Object.defineProperty(exports, "getHelpFlagAdditions", { enumerable: true, get: function () { return util_3.getHelpFlagAdditions; } });
25
19
  function getHelpSubject(args, config) {
26
20
  // for each help flag that starts with '--' create a new flag with same name sans '--'
27
- const mergedHelpFlags = getHelpFlagAdditions(config);
21
+ const mergedHelpFlags = (0, util_2.getHelpFlagAdditions)(config);
28
22
  for (const arg of args) {
29
23
  if (arg === '--')
30
24
  return;
@@ -76,7 +70,8 @@ class Help extends HelpBase {
76
70
  return topics;
77
71
  }
78
72
  async showHelp(argv) {
79
- argv = argv.filter(arg => !getHelpFlagAdditions(this.config).includes(arg));
73
+ const originalArgv = argv.slice(1);
74
+ argv = argv.filter(arg => !(0, util_2.getHelpFlagAdditions)(this.config).includes(arg));
80
75
  if (this.config.topicSeparator !== ':')
81
76
  argv = (0, util_2.standardizeIDFromArgv)(argv, this.config);
82
77
  const subject = getHelpSubject(argv, this.config);
@@ -105,6 +100,14 @@ class Help extends HelpBase {
105
100
  await this.showTopicHelp(topic);
106
101
  return;
107
102
  }
103
+ if (this.config.flexibleTaxonomy) {
104
+ const matches = this.config.findMatches(subject, originalArgv);
105
+ if (matches.length > 0) {
106
+ const result = await this.config.runHook('command_incomplete', { id: subject, argv: originalArgv, matches });
107
+ if (result.successes.length > 0)
108
+ return;
109
+ }
110
+ }
108
111
  (0, errors_1.error)(`Command ${subject} not found.`);
109
112
  }
110
113
  async getDynamicCommand(cmdName) {
@@ -8,4 +8,5 @@ export declare function template(context: any): (t: string) => string;
8
8
  export declare function toStandardizedId(commandID: string, config: IConfig): string;
9
9
  export declare function toConfiguredId(commandID: string, config: IConfig): string;
10
10
  export declare function standardizeIDFromArgv(argv: string[], config: IConfig): string[];
11
+ export declare function getHelpFlagAdditions(config: IConfig): string[];
11
12
  export {};
package/lib/help/util.js CHANGED
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.standardizeIDFromArgv = exports.toConfiguredId = exports.toStandardizedId = exports.template = exports.loadHelpClass = void 0;
3
+ exports.getHelpFlagAdditions = exports.standardizeIDFromArgv = exports.toConfiguredId = exports.toStandardizedId = exports.template = exports.loadHelpClass = void 0;
4
4
  const ejs = require("ejs");
5
5
  const _1 = require(".");
6
6
  const module_loader_1 = require("../module-loader");
7
+ const util_1 = require("../config/util");
7
8
  function extractClass(exported) {
8
9
  return exported && exported.default ? exported.default : exported;
9
10
  }
@@ -32,22 +33,20 @@ exports.template = template;
32
33
  function collateSpacedCmdIDFromArgs(argv, config) {
33
34
  if (argv.length === 1)
34
35
  return argv;
35
- const ids = new Set(config.commandIDs.concat(config.topics.map(t => t.name)));
36
+ const ids = (0, util_1.collectUsableIds)(config.commandIDs);
36
37
  const findId = (argv) => {
37
38
  const final = [];
38
- const idPresent = (id) => ids.has(id);
39
+ const idPresent = (id) => ids.includes(id);
39
40
  const isFlag = (s) => s.startsWith('-');
40
41
  const isArgWithValue = (s) => s.includes('=');
41
42
  const finalizeId = (s) => s ? [...final, s].join(':') : final.join(':');
42
43
  const hasSubCommandsWithArgs = () => {
43
44
  const id = finalizeId();
44
- /**
45
- * Get a list of sub commands for the current command id. A command is returned as a subcommand under either
46
- * of these conditions:
47
- * 1. the `id` start with the current command id.
48
- * 2. any of the aliases start with the current command id.
49
- */
50
- const subCommands = config.commands.filter(c => (c.id).startsWith(id) || c.aliases.some(a => a.startsWith(id)));
45
+ if (!id)
46
+ return false;
47
+ // Get a list of sub commands for the current command id. A command is returned as a subcommand if the `id` starts with the current command id.
48
+ // e.g. `foo:bar` is a subcommand of `foo`
49
+ const subCommands = config.commands.filter(c => (c.id).startsWith(id));
51
50
  return Boolean(subCommands.find(cmd => { var _a; return cmd.strict === false || ((_a = cmd.args) === null || _a === void 0 ? void 0 : _a.length) > 0; }));
52
51
  };
53
52
  for (const arg of argv) {
@@ -88,3 +87,10 @@ function standardizeIDFromArgv(argv, config) {
88
87
  return argv;
89
88
  }
90
89
  exports.standardizeIDFromArgv = standardizeIDFromArgv;
90
+ function getHelpFlagAdditions(config) {
91
+ var _a;
92
+ const helpFlags = ['--help'];
93
+ const additionalHelpFlags = (_a = config.pjson.oclif.additionalHelpFlags) !== null && _a !== void 0 ? _a : [];
94
+ return [...new Set([...helpFlags, ...additionalHelpFlags]).values()];
95
+ }
96
+ exports.getHelpFlagAdditions = getHelpFlagAdditions;
@@ -86,6 +86,7 @@ export interface Config {
86
86
  plugins: Plugin[];
87
87
  binPath?: string;
88
88
  valid: boolean;
89
+ flexibleTaxonomy?: boolean;
89
90
  topicSeparator: ':' | ' ';
90
91
  readonly commands: Command.Plugin[];
91
92
  readonly topics: Topic[];
@@ -93,6 +94,8 @@ export interface Config {
93
94
  runCommand<T = unknown>(id: string, argv?: string[]): Promise<T>;
94
95
  runCommand<T = unknown>(id: string, argv?: string[], cachedCommand?: Command.Plugin): Promise<T>;
95
96
  runHook<T extends keyof Hooks>(event: T, opts: Hooks[T]['options'], timeout?: number): Promise<Hook.Result<Hooks[T]['return']>>;
97
+ getAllCommandIDs(): string[];
98
+ getAllCommands(): Command.Plugin[];
96
99
  findCommand(id: string, opts: {
97
100
  must: true;
98
101
  }): Command.Plugin;
@@ -105,6 +108,7 @@ export interface Config {
105
108
  findTopic(id: string, opts?: {
106
109
  must: boolean;
107
110
  }): Topic | undefined;
111
+ findMatches(id: string, argv: string[]): Command.Plugin[];
108
112
  scopedEnvVar(key: string): string | undefined;
109
113
  scopedEnvVarKey(key: string): string;
110
114
  scopedEnvVarTrue(key: string): boolean;
@@ -50,6 +50,14 @@ export interface Hooks {
50
50
  };
51
51
  return: unknown;
52
52
  };
53
+ 'command_incomplete': {
54
+ options: {
55
+ id: string;
56
+ argv: string[];
57
+ matches: Command.Plugin[];
58
+ };
59
+ return: unknown;
60
+ };
53
61
  'plugins:preinstall': {
54
62
  options: {
55
63
  plugin: {
@@ -75,6 +83,7 @@ export declare namespace Hook {
75
83
  type Preupdate = Hook<'preupdate'>;
76
84
  type Update = Hook<'update'>;
77
85
  type CommandNotFound = Hook<'command_not_found'>;
86
+ type CommandIncomplete = Hook<'command_incomplete'>;
78
87
  interface Context {
79
88
  config: Config;
80
89
  exit(code?: number): void;
@@ -16,6 +16,7 @@ export declare namespace PJSON {
16
16
  schema?: number;
17
17
  description?: string;
18
18
  topicSeparator?: ':' | ' ';
19
+ flexibleTaxonomy?: boolean;
19
20
  hooks?: {
20
21
  [name: string]: (string | string[]);
21
22
  };
@@ -78,6 +79,7 @@ export declare namespace PJSON {
78
79
  npmRegistry?: string;
79
80
  scope?: string;
80
81
  dirname?: string;
82
+ flexibleTaxonomy?: boolean;
81
83
  };
82
84
  }
83
85
  interface User extends PJSON {
package/lib/main.js CHANGED
@@ -62,7 +62,7 @@ async function run(argv = process.argv.slice(2), options) {
62
62
  // find & run command
63
63
  const cmd = config.findCommand(id);
64
64
  if (!cmd) {
65
- const topic = config.findTopic(id);
65
+ const topic = config.flexibleTaxonomy ? null : config.findTopic(id);
66
66
  if (topic)
67
67
  return config.runCommand('help', [id]);
68
68
  if (config.pjson.oclif.default) {
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@oclif/core",
3
3
  "description": "base library for oclif CLIs",
4
- "version": "1.5.2",
4
+ "version": "1.6.1",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/core/issues",
7
7
  "dependencies": {
8
8
  "@oclif/linewrap": "^1.0.0",
9
9
  "@oclif/screen": "^3.0.2",
10
- "ansi-escapes": "^4.3.0",
11
- "ansi-styles": "^4.2.0",
10
+ "ansi-escapes": "^4.3.2",
11
+ "ansi-styles": "^4.3.0",
12
12
  "cardinal": "^2.1.1",
13
13
  "chalk": "^4.1.2",
14
14
  "clean-stack": "^3.0.1",
@@ -17,14 +17,14 @@
17
17
  "ejs": "^3.1.6",
18
18
  "fs-extra": "^9.1.0",
19
19
  "get-package-type": "^0.1.0",
20
- "globby": "^11.0.4",
20
+ "globby": "^11.1.0",
21
21
  "hyperlinker": "^1.0.0",
22
22
  "indent-string": "^4.0.0",
23
23
  "is-wsl": "^2.2.0",
24
- "js-yaml": "^3.13.1",
24
+ "js-yaml": "^3.14.1",
25
25
  "lodash": "^4.17.21",
26
26
  "natural-orderby": "^2.0.3",
27
- "object-treeify": "^1.1.4",
27
+ "object-treeify": "^1.1.33",
28
28
  "password-prompt": "^1.1.2",
29
29
  "semver": "^7.3.5",
30
30
  "string-width": "^4.2.3",
@@ -37,50 +37,46 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@commitlint/config-conventional": "^12.1.4",
40
- "@oclif/plugin-help": "^5.1.7",
41
- "@oclif/plugin-plugins": "^2.0.8",
42
- "@oclif/test": "^1.2.8",
40
+ "@oclif/plugin-help": "^5.1.11",
41
+ "@oclif/plugin-plugins": "^2.1.0",
42
+ "@oclif/test": "^2.1.0",
43
43
  "@types/ansi-styles": "^3.2.1",
44
- "@types/chai": "^4.2.22",
45
- "@types/chai-as-promised": "^7.1.4",
44
+ "@types/chai": "^4.3.0",
45
+ "@types/chai-as-promised": "^7.1.5",
46
46
  "@types/clean-stack": "^2.1.1",
47
47
  "@types/cli-progress": "^3.9.2",
48
48
  "@types/ejs": "^3.1.0",
49
49
  "@types/fs-extra": "^9.0.13",
50
50
  "@types/indent-string": "^4.0.1",
51
- "@types/js-yaml": "^3.12.1",
52
- "@types/lodash": "^4.14.117",
51
+ "@types/js-yaml": "^3.12.7",
52
+ "@types/lodash": "^4.14.178",
53
53
  "@types/mocha": "^8.2.3",
54
54
  "@types/nock": "^11.1.0",
55
55
  "@types/node": "^15.14.9",
56
56
  "@types/node-notifier": "^8.0.2",
57
57
  "@types/proxyquire": "^1.3.28",
58
58
  "@types/semver": "^7.3.9",
59
- "@types/shelljs": "^0.8.10",
59
+ "@types/shelljs": "^0.8.11",
60
60
  "@types/strip-ansi": "^5.2.1",
61
61
  "@types/supports-color": "^8.1.1",
62
62
  "@types/wrap-ansi": "^3.0.0",
63
- "chai": "^4.3.4",
63
+ "chai": "^4.3.6",
64
64
  "chai-as-promised": "^7.1.1",
65
65
  "commitlint": "^12.1.4",
66
66
  "eslint": "^7.32.0",
67
67
  "eslint-config-oclif": "^4.0.0",
68
68
  "eslint-config-oclif-typescript": "^1.0.2",
69
69
  "fancy-test": "^1.4.10",
70
- "globby": "^11.0.4",
70
+ "globby": "^11.1.0",
71
71
  "husky": "6",
72
72
  "mocha": "^8.4.0",
73
- "nock": "^13.2.1",
73
+ "nock": "^13.2.4",
74
74
  "proxyquire": "^2.1.3",
75
- "shelljs": "^0.8.4",
76
- "shx": "^0.3.3",
75
+ "shelljs": "^0.8.5",
76
+ "shx": "^0.3.4",
77
77
  "sinon": "^11.1.2",
78
78
  "ts-node": "^9.1.1",
79
- "typescript": "4.4.4"
80
- },
81
- "resolutions": {
82
- "@oclif/command": "1.8.9",
83
- "@oclif/parser": "3.8.6"
79
+ "typescript": "4.5.5"
84
80
  },
85
81
  "engines": {
86
82
  "node": ">=12.0.0"