Repository: oclif/core
Pull request #819 (2023-10-11 19:42:58)

feat: add hideAliases help option
- Adds `hideAliasesFromRoot` to `HelpOptions`. When `true`, it filters command aliases from the `COMMANDS` section of the root help
- Adds `hiddenAliases` property to `Command` allowing you to create command aliases that won't be visible in the help output

Fixes https://github.com/oclif/oclif/issues/888


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf16329..9db2095 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,3 @@
-## [3.0.7](https://github.com/oclif/core/compare/3.0.6...3.0.7) (2023-10-12)
-
-
-### Bug Fixes
-
-* use _flags for linked v2 plugins ([#820](https://github.com/oclif/core/issues/820)) ([cc63720](https://github.com/oclif/core/commit/cc63720aa90163d28dda9b5fb258ccc0556b6931))
-
-
-
 ## [3.0.6](https://github.com/oclif/core/compare/3.0.5...3.0.6) (2023-10-11)
 
 
diff --git a/package.json b/package.json
index 0de07eb..b07a170 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@oclif/core",
   "description": "base library for oclif CLIs",
-  "version": "3.0.7",
+  "version": "3.0.6",
   "author": "Salesforce",
   "bugs": "https://github.com/oclif/core/issues",
   "dependencies": {
diff --git a/src/command.ts b/src/command.ts
index 4b4c0e9..bef7ea9 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -98,6 +98,9 @@ export abstract class Command {
   /** Hide the command from help */
   public static hidden: boolean
 
+  /** An array of aliases for this command that are hidden from help. */
+  public static hiddenAliases: string[] = []
+
   /** A command ID, used mostly in error or verbose reporting. */
   public static id: string
 
@@ -414,6 +417,7 @@ export namespace Command {
     flags: {[name: string]: Flag.Cached}
     hasDynamicHelp?: boolean
     hidden: boolean
+    hiddenAliases: string[]
     id: string
     isESM?: boolean
     permutations?: string[]
diff --git a/src/config/config.ts b/src/config/config.ts
index c1409e2..85abff0 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -784,17 +784,15 @@ export class Config implements IConfig {
         this.commandPermutations.add(permutation, command.id)
       }
 
-      // set command aliases
-      for (const alias of command.aliases ?? []) {
+      const handleAlias = (alias: string, hidden = false) => {
         if (this._commands.has(alias)) {
           const prioritizedCommand = this.determinePriority([this._commands.get(alias)!, command])
           this._commands.set(alias, {...prioritizedCommand, id: alias})
         } else {
-          this._commands.set(alias, {...command, id: alias})
+          this._commands.set(alias, {...command, hidden, id: alias})
         }
 
         // set every permutation of the aliases
-
         // v3 moved command alias permutations to the manifest, but some plugins may not have
         // the new manifest yet. For those, we need to calculate the permutations here.
         const aliasPermutations =
@@ -806,6 +804,16 @@ export class Config implements IConfig {
           this.commandPermutations.add(permutation, command.id)
         }
       }
+
+      // set command aliases
+      for (const alias of command.aliases ?? []) {
+        handleAlias(alias)
+      }
+
+      // set hidden command aliases
+      for (const alias of command.hiddenAliases ?? []) {
+        handleAlias(alias, true)
+      }
     }
 
     marker?.addDetails({commandCount: plugin.commands.length})
diff --git a/src/flags.ts b/src/flags.ts
index 15183df..55c15b9 100644
--- a/src/flags.ts
+++ b/src/flags.ts
@@ -1,4 +1,3 @@
-/* eslint-disable valid-jsdoc */
 import {URL} from 'node:url'
 
 import {CLIError} from './errors'
@@ -140,7 +139,9 @@ export const help = (opts: Partial<BooleanFlag<boolean>> = {}): BooleanFlag<void
     ...opts,
     async parse(_, cmd) {
       const Help = await loadHelpClass(cmd.config)
-      await new Help(cmd.config, cmd.config.pjson.helpOptions).showHelp(cmd.id ? [cmd.id, ...cmd.argv] : cmd.argv)
+      await new Help(cmd.config, cmd.config.pjson.oclif.helpOptions ?? cmd.config.pjson.helpOptions).showHelp(
+        cmd.id ? [cmd.id, ...cmd.argv] : cmd.argv,
+      )
       cmd.exit(0)
     },
   })
diff --git a/src/help/index.ts b/src/help/index.ts
index 0e28f93..c1f6ea7 100644
--- a/src/help/index.ts
+++ b/src/help/index.ts
@@ -98,12 +98,13 @@ export class Help extends HelpBase {
 
   protected formatCommands(commands: Array<Command.Loadable>): string {
     if (commands.length === 0) return ''
-
     const body = this.renderList(
-      commands.map((c) => {
-        if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator)
-        return [c.id, this.summary(c)]
-      }),
+      commands
+        .filter((c) => (this.opts.hideAliasesFromRoot ? !c.aliases?.includes(c.id) : true))
+        .map((c) => {
+          if (this.config.topicSeparator !== ':') c.id = c.id.replaceAll(':', this.config.topicSeparator)
+          return [c.id, this.summary(c)]
+        }),
       {
         indentation: 2,
         spacer: '\n',
diff --git a/src/interfaces/help.ts b/src/interfaces/help.ts
index 23023a6..f0ca5c0 100644
--- a/src/interfaces/help.ts
+++ b/src/interfaces/help.ts
@@ -4,6 +4,10 @@ export interface HelpOptions {
    * Use docopts as the usage. Defaults to true.
    */
   docopts?: boolean
+  /**
+   * If true, hide command aliases from the root help output. Defaults to false.
+   */
+  hideAliasesFromRoot?: boolean
   /**
    * By default, the command summary is show at the top of the help and as the first line in
    * the command description. Repeating the summary in the command description improves readability
diff --git a/src/main.ts b/src/main.ts
index 4d9e155..169d6e3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -70,7 +70,7 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr
   // display help version if applicable
   if (helpAddition(argv, config)) {
     const Help = await loadHelpClass(config)
-    const help = new Help(config, config.pjson.helpOptions)
+    const help = new Help(config, config.pjson.oclif.helpOptions ?? config.pjson.helpOptions)
     await help.showHelp(argv)
     await collectPerf()
     return
diff --git a/src/util/cache-command.ts b/src/util/cache-command.ts
index a49f92d..e48fe61 100644
--- a/src/util/cache-command.ts
+++ b/src/util/cache-command.ts
@@ -95,15 +95,17 @@ export async function cacheCommand(
   const args = await cacheArgs(ensureArgObject(cmd.args), respectNoCacheDefault)
 
   const stdProperties = {
-    aliases: cmd.aliases || [],
+    aliases: cmd.aliases ?? [],
     args,
     deprecateAliases: cmd.deprecateAliases,
     deprecationOptions: cmd.deprecationOptions,
     description: cmd.description,
-    examples: cmd.examples || (cmd as any).example,
+    // Support both `examples` and `example` for backwards compatibility.
+    examples: cmd.examples ?? (cmd as unknown as {example: string}).example,
     flags,
     hasDynamicHelp: Object.values(flags).some((f) => f.hasDynamicHelp),
     hidden: cmd.hidden,
+    hiddenAliases: cmd.hiddenAliases ?? [],
     id: cmd.id,
     pluginAlias: plugin && plugin.alias,
     pluginName: plugin && plugin.name,


Test patch:
diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts
index 1f51573..2d95ea6 100644
--- a/test/config/config.flexible.test.ts
+++ b/test/config/config.flexible.test.ts
@@ -58,6 +58,7 @@ describe('Config with flexible taxonomy', () => {
         flagA: Flags.boolean({char: 'a'}),
       },
       hidden: false,
+      hiddenAliases: [],
       id: commandIds[0],
       async load(): Promise<Command.Class> {
         return MyCommandClass
@@ -74,6 +75,7 @@ describe('Config with flexible taxonomy', () => {
         flagB: Flags.boolean({}),
       },
       hidden: false,
+      hiddenAliases: [],
       id: commandIds[1],
       async load(): Promise<Command.Class> {
         return MyCommandClass
diff --git a/test/config/config.test.ts b/test/config/config.test.ts
index cc4b4b2..82f014a 100644
--- a/test/config/config.test.ts
+++ b/test/config/config.test.ts
@@ -251,6 +251,7 @@ describe('Config', () => {
         args: {},
         flags: {},
         hidden: false,
+        hiddenAliases: [],
         id: commandIds[0],
         async load(): Promise<Command.Class> {
           return MyCommandClass
@@ -264,6 +265,7 @@ describe('Config', () => {
         args: {},
         flags: {},
         hidden: false,
+        hiddenAliases: [],
         id: commandIds[1],
         async load(): Promise<Command.Class> {
           return MyCommandClass
diff --git a/test/help/fixtures/fixtures.ts b/test/help/fixtures/fixtures.ts
index 9ff601f..fe05e21 100644
--- a/test/help/fixtures/fixtures.ts
+++ b/test/help/fixtures/fixtures.ts
@@ -154,3 +154,12 @@ export class LongDescription extends Command {
     'run'
   }
 }
+
+export class CommandWithAliases extends Command {
+  static aliases = ['bar', 'baz', 'qux']
+  static description = 'This is a command with aliases'
+  static id = 'foo'
+  async run(): Promise<void> {
+    'run'
+  }
+}
diff --git a/test/help/show-help.test.ts b/test/help/show-help.test.ts
index 9e44775..01db70f 100644
--- a/test/help/show-help.test.ts
+++ b/test/help/show-help.test.ts
@@ -13,6 +13,7 @@ import {
   AppsDestroy,
   AppsIndex,
   AppsTopic,
+  CommandWithAliases,
   DbCreate,
   DbTopic,
   DeprecateAliases,
@@ -135,6 +136,69 @@ USAGE
 COMMANDS
   apps  List all apps (app index command)`)
     })
+
+  test
+    .loadConfig()
+    .stdout()
+    .do(async (ctx) => {
+      const {config} = ctx
+
+      monkeyPatchCommands(config, [
+        {
+          name: 'plugin-1',
+          commands: [CommandWithAliases],
+          topics: [],
+        },
+      ])
+
+      const help = new TestHelp(config as any, {hideAliasesFromRoot: true})
+      await help.showHelp([])
+    })
+    .it('shows root help without aliases if hideAliasesFromRoot=true', ({stdout, config}) => {
+      expect(stdout.trim()).to.equal(`base library for oclif CLIs
+
+VERSION
+  ${config.userAgent}
+
+USAGE
+  $ oclif [COMMAND]
+
+COMMANDS
+  foo  This is a command with aliases`)
+    })
+
+  test
+    .loadConfig()
+    .stdout()
+    .do(async (ctx) => {
+      const {config} = ctx
+
+      monkeyPatchCommands(config, [
+        {
+          name: 'plugin-1',
+          commands: [CommandWithAliases],
+          topics: [],
+        },
+      ])
+
+      const help = new TestHelp(config as any)
+      await help.showHelp([])
+    })
+    .it('shows root help with aliases commands by default', ({stdout, config}) => {
+      expect(stdout.trim()).to.equal(`base library for oclif CLIs
+
+VERSION
+  ${config.userAgent}
+
+USAGE
+  $ oclif [COMMAND]
+
+COMMANDS
+  bar  This is a command with aliases
+  baz  This is a command with aliases
+  foo  This is a command with aliases
+  qux  This is a command with aliases`)
+    })
 })
 
 describe('showHelp for a topic', () => {
diff --git a/test/integration/util.ts b/test/integration/util.ts
index 005f92c..fc2c306 100644
--- a/test/integration/util.ts
+++ b/test/integration/util.ts
@@ -113,7 +113,6 @@ export class Executor {
   }
 }
 
-// eslint-disable-next-line valid-jsdoc
 /**
  * Setup for integration tests.
  *
diff --git a/test/util/cache-command.test.ts b/test/util/cache-command.test.ts
index 685bd86..7b7e124 100644
--- a/test/util/cache-command.test.ts
+++ b/test/util/cache-command.test.ts
@@ -59,6 +59,7 @@ describe('cacheCommand', () => {
       state: undefined,
       description: 'test command',
       aliases: ['alias1', 'alias2'],
+      hiddenAliases: [],
       title: 'cmd title',
       usage: ['$ usage'],
       examples: undefined,
@@ -193,6 +194,7 @@ describe('cacheCommand', () => {
       hidden: undefined,
       state: 'beta',
       aliases: ['base'],
+      hiddenAliases: [],
       examples: undefined,
       deprecationOptions: undefined,
       deprecateAliases: undefined,