From b2044660a98fac5935c0831507a96d35a7a775f3 Mon Sep 17 00:00:00 2001 From: "Md. Mohiuddin Khaled Maruf" Date: Wed, 14 Jan 2026 18:44:44 +0600 Subject: [PATCH] feat: add flag aliases support to autocomplete completions Flag definitions can include an `aliases` array property that allows alternative names for the same flag (e.g., --no-progress and --noProgress). Previously, only the primary flag name was included in generated autocomplete scripts. This change ensures flag aliases are included in the completion output for all supported shells: - zsh: aliases added to _arguments block - bash: aliases added to the commands list - powershell: aliases added to the flags hashtable Example output from a dummy cli: ```bash $ my-cli app result - --bump-dev-version --bumpDevVersion -- Bump dev version before building. --help -- Show help for command --no-progress --noProgress -- Don't display any progress indicators --path -p -- The root directory of the app ``` Closes #1088 --- src/autocomplete/powershell.ts | 17 ++++ src/autocomplete/zsh.ts | 22 ++++++ src/commands/autocomplete/create.ts | 56 +++++++++---- test/autocomplete/powershell.test.ts | 91 ++++++++++++++++++++++ test/autocomplete/zsh.test.ts | 90 +++++++++++++++++++++ test/commands/autocomplete/create.test.ts | 44 +++++++++++ test/test.oclif.manifest.flag-aliases.json | 28 +++++++ 7 files changed, 331 insertions(+), 17 deletions(-) create mode 100644 test/test.oclif.manifest.flag-aliases.json diff --git a/src/autocomplete/powershell.ts b/src/autocomplete/powershell.ts index 46b8b582..9115b49b 100644 --- a/src/autocomplete/powershell.ts +++ b/src/autocomplete/powershell.ts @@ -297,6 +297,23 @@ Register-ArgumentCompleter -Native -CommandName ${ } else { flaghHashtables.push(` "${f.name}" = @{ "summary" = "${flagSummary}" }`) } + + // Add flag aliases + const aliases = (f as any).aliases as string[] | undefined + if (aliases && aliases.length > 0) { + for (const alias of aliases) { + if (f.type === 'option' && f.multiple) { + flaghHashtables.push( + ` "${alias}" = @{ + "summary" = "${flagSummary}" + "multiple" = $true +}`, + ) + } else { + flaghHashtables.push(` "${alias}" = @{ "summary" = "${flagSummary}" }`) + } + } + } } } diff --git a/src/autocomplete/zsh.ts b/src/autocomplete/zsh.ts index 115dca9f..41ed2637 100644 --- a/src/autocomplete/zsh.ts +++ b/src/autocomplete/zsh.ts @@ -177,6 +177,28 @@ _${this.config.bin} flagSpec += ' \\\n' argumentsBlock += flagSpec + + // Add completions for flag aliases + const aliases = (f as any).aliases as string[] | undefined + if (aliases && aliases.length > 0) { + for (const alias of aliases) { + let aliasSpec = '' + if (f.type === 'option') { + if (f.multiple) { + aliasSpec += '"*"' + } + + aliasSpec += `--${alias}"[${flagSummary}]:` + aliasSpec += f.options ? `${f.name} options:(${f.options.join(' ')})"` : 'file:_files"' + } else { + // Flag.Boolean + aliasSpec += `--${alias}"[${flagSummary}]"` + } + + aliasSpec += ' \\\n' + argumentsBlock += aliasSpec + } + } } // add global `--help` flag diff --git a/src/commands/autocomplete/create.ts b/src/commands/autocomplete/create.ts index 983e3325..fb3bd900 100644 --- a/src/commands/autocomplete/create.ts +++ b/src/commands/autocomplete/create.ts @@ -262,25 +262,47 @@ compinit;\n` private genCmdPublicFlags(Command: CommandCompletion): string { const Flags = Command.flags || {} - return Object.keys(Flags) - .filter((flag) => !Flags[flag].hidden) - .map((flag) => `--${flag}`) - .join(' ') + const flagList: string[] = [] + for (const flag of Object.keys(Flags)) { + if (Flags[flag].hidden) continue + flagList.push(`--${flag}`) + // Add flag aliases + const aliases = (Flags[flag] as any).aliases as string[] | undefined + if (aliases && aliases.length > 0) { + for (const alias of aliases) { + flagList.push(`--${alias}`) + } + } + } + + return flagList.join(' ') } private genZshFlagSpecs(Klass: any): string { - return Object.keys(Klass.flags || {}) - .filter((flag) => Klass.flags && !Klass.flags[flag].hidden) - .map((flag) => { - const f = (Klass.flags && Klass.flags[flag]) || {description: ''} - const isBoolean = f.type === 'boolean' - const isOption = f.type === 'option' - const name = isBoolean ? flag : `${flag}=-` - const multiple = isOption && f.multiple ? '*' : '' - const valueCmpl = isBoolean ? '' : ':' - const completion = `${multiple}--${name}[${sanitizeDescription(f.summary || f.description)}]${valueCmpl}` - return `"${completion}"` - }) - .join('\n') + const specs: string[] = [] + for (const flag of Object.keys(Klass.flags || {})) { + if (Klass.flags && Klass.flags[flag].hidden) continue + const f = (Klass.flags && Klass.flags[flag]) || {description: ''} + const isBoolean = f.type === 'boolean' + const isOption = f.type === 'option' + const name = isBoolean ? flag : `${flag}=-` + const multiple = isOption && f.multiple ? '*' : '' + const valueCmpl = isBoolean ? '' : ':' + const summary = sanitizeDescription(f.summary || f.description) + const completion = `${multiple}--${name}[${summary}]${valueCmpl}` + specs.push(`"${completion}"`) + + // Add flag aliases + const aliases = (f as any).aliases as string[] | undefined + if (aliases && aliases.length > 0) { + for (const alias of aliases) { + const aliasName = isBoolean ? alias : `${alias}=-` + const aliasCompletion = `${multiple}--${aliasName}[${summary}]${valueCmpl}` + specs.push(`"${aliasCompletion}"`) + } + } + } + + return specs.join('\n') } } diff --git a/test/autocomplete/powershell.test.ts b/test/autocomplete/powershell.test.ts index 25283d0e..c77b5049 100644 --- a/test/autocomplete/powershell.test.ts +++ b/test/autocomplete/powershell.test.ts @@ -144,6 +144,39 @@ const commandPluginD: Command.Loadable = { summary: 'execute code', } +// Command with flag aliases for testing +const commandWithFlagAliases: Command.Loadable = { + aliases: [], + args: {}, + flags: { + 'no-progress': { + aliases: ['noProgress'], + allowNo: false, + name: 'no-progress', + summary: 'Disable progress output.', + type: 'boolean', + }, + 'use-cache': { + aliases: ['useCache', 'cache'], + char: 'c', + multiple: false, + name: 'use-cache', + summary: 'Use cached data.', + type: 'option', + }, + }, + hidden: false, + hiddenAliases: [], + id: 'build', + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class + }, + pluginAlias: '@My/plugine', + pluginType: 'core', + strict: false, + summary: 'Build a project', +} + const pluginA: IPlugin = { _base: '', alias: '@My/plugina', @@ -771,4 +804,62 @@ $scriptblock = { Register-ArgumentCompleter -Native -CommandName @("test","test1","test-cli") -ScriptBlock $scriptblock `) }) + + describe('powershell completion with flag aliases', () => { + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json') + const config = new Config({root}) + + const pluginWithFlagAliases: IPlugin = { + _base: '', + alias: '@My/pluginWithFlagAliases', + commandIDs: ['build'], + commands: [commandWithFlagAliases], + commandsDir: '', + async findCommand(): Promise { + return new MyCommandClass() as unknown as Command.Class + }, + hasManifest: false, + hooks: {}, + isRoot: false, + async load(): Promise {}, + moduleType: 'commonjs', + name: '@My/pluginWithFlagAliases', + options: {root: ''}, + pjson: {} as any, + root: '', + tag: 'tag', + topics: [], + type: 'core', + valid: true, + version: '0.0.0', + } + + before(async () => { + await config.load() + config.plugins.set(pluginWithFlagAliases.name, pluginWithFlagAliases) + config.plugins.delete('@oclif/plugin-autocomplete') + for (const plugin of config.getPluginsList()) { + // @ts-expect-error private method + config.loadCommands(plugin) + // @ts-expect-error private method + config.loadTopics(plugin) + } + }) + + it('includes flag aliases in the completion output', () => { + config.bin = 'test-cli' + config.binAliases = undefined + const powerShellComp = new PowerShellComp(config as Config) + const output = powerShellComp.generate() + + // Check that main flags are present + expect(output).to.include('"no-progress" = @{ "summary" = "Disable progress output." }') + expect(output).to.include('"use-cache" = @{ "summary" = "Use cached data." }') + + // Check that flag aliases are present + expect(output).to.include('"noProgress" = @{ "summary" = "Disable progress output." }') + expect(output).to.include('"useCache" = @{ "summary" = "Use cached data." }') + expect(output).to.include('"cache" = @{ "summary" = "Use cached data." }') + }) + }) }) diff --git a/test/autocomplete/zsh.test.ts b/test/autocomplete/zsh.test.ts index df92261a..fd51a98f 100644 --- a/test/autocomplete/zsh.test.ts +++ b/test/autocomplete/zsh.test.ts @@ -146,6 +146,39 @@ const commandPluginD: Command.Loadable = { summary: 'execute code', } +// Command with flag aliases for testing +const commandWithFlagAliases: Command.Loadable = { + aliases: [], + args: {}, + flags: { + 'no-progress': { + aliases: ['noProgress'], + allowNo: false, + name: 'no-progress', + summary: 'Disable progress output.', + type: 'boolean', + }, + 'use-cache': { + aliases: ['useCache', 'cache'], + char: 'c', + multiple: false, + name: 'use-cache', + summary: 'Use cached data.', + type: 'option', + }, + }, + hidden: false, + hiddenAliases: [], + id: 'build', + async load(): Promise { + return new MyCommandClass() as unknown as Command.Class + }, + pluginAlias: '@My/plugine', + pluginType: 'core', + strict: false, + summary: 'Build a project', +} + const pluginA: IPlugin = { _base: '', alias: '@My/plugina', @@ -602,4 +635,61 @@ _test-cli `) }) }) + + describe('zsh completion with flag aliases', () => { + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json') + const config = new Config({root}) + + const pluginWithFlagAliases: IPlugin = { + _base: '', + alias: '@My/pluginWithFlagAliases', + commandIDs: ['build'], + commands: [commandWithFlagAliases], + commandsDir: '', + async findCommand(): Promise { + return new MyCommandClass() as unknown as Command.Class + }, + hasManifest: false, + hooks: {}, + isRoot: false, + async load(): Promise {}, + moduleType: 'commonjs', + name: '@My/pluginWithFlagAliases', + options: {root: ''}, + pjson: {} as any, + root: '', + tag: 'tag', + topics: [], + type: 'core', + valid: true, + version: '0.0.0', + } + + before(async () => { + await config.load() + config.plugins.set(pluginWithFlagAliases.name, pluginWithFlagAliases) + config.plugins.delete('@oclif/plugin-autocomplete') + for (const plugin of config.getPluginsList()) { + // @ts-expect-error private method + config.loadCommands(plugin) + // @ts-expect-error private method + config.loadTopics(plugin) + } + }) + + it('includes flag aliases in the completion output', () => { + config.bin = 'test-cli' + const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) + const output = zshCompWithSpaces.generate() + + // Check that main flags are present + expect(output).to.include('--no-progress') + expect(output).to.include('--use-cache') + + // Check that flag aliases are present + expect(output).to.include('--noProgress') + expect(output).to.include('--useCache') + expect(output).to.include('--cache') + }) + }) }) diff --git a/test/commands/autocomplete/create.test.ts b/test/commands/autocomplete/create.test.ts index c4455ce3..d68027b1 100644 --- a/test/commands/autocomplete/create.test.ts +++ b/test/commands/autocomplete/create.test.ts @@ -272,5 +272,49 @@ _oclif-example\n`) /* eslint-enable no-useless-escape */ }) + + it('#bashCompletionFunction includes flag aliases', async () => { + const aliasConfig = new Config({root}) + await aliasConfig.load() + const aliasCmd: any = new Create([], aliasConfig) + const aliasPlugin: any = new Plugin({root}) + aliasCmd.config.plugins = [aliasPlugin] + aliasPlugin._manifest = () => + readJson( + path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../test.oclif.manifest.flag-aliases.json'), + ) + await aliasPlugin.load() + + const bashOutput = aliasCmd.bashCompletionFunction as string + // Check that main flags are present + expect(bashOutput).to.include('--no-progress') + expect(bashOutput).to.include('--use-cache') + // Check that flag aliases are present + expect(bashOutput).to.include('--noProgress') + expect(bashOutput).to.include('--useCache') + expect(bashOutput).to.include('--cache') + }) + + it('#zshCompletionFunction includes flag aliases', async () => { + const aliasConfig = new Config({root}) + await aliasConfig.load() + const aliasCmd: any = new Create([], aliasConfig) + const aliasPlugin: any = new Plugin({root}) + aliasCmd.config.plugins = [aliasPlugin] + aliasPlugin._manifest = () => + readJson( + path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../test.oclif.manifest.flag-aliases.json'), + ) + await aliasPlugin.load() + + const zshOutput = aliasCmd.zshCompletionFunction as string + // Check that main flags are present + expect(zshOutput).to.include('--no-progress') + expect(zshOutput).to.include('--use-cache') + // Check that flag aliases are present + expect(zshOutput).to.include('--noProgress') + expect(zshOutput).to.include('--useCache') + expect(zshOutput).to.include('--cache') + }) }) }) diff --git a/test/test.oclif.manifest.flag-aliases.json b/test/test.oclif.manifest.flag-aliases.json new file mode 100644 index 00000000..ad6fa17b --- /dev/null +++ b/test/test.oclif.manifest.flag-aliases.json @@ -0,0 +1,28 @@ +{ + "version": "0.0.0", + "commands": { + "build": { + "id": "build", + "description": "build a project", + "pluginName": "@oclif/plugin-autocomplete", + "pluginType": "core", + "aliases": [], + "flags": { + "no-progress": { + "name": "no-progress", + "type": "boolean", + "description": "disable progress output", + "aliases": ["noProgress"] + }, + "use-cache": { + "name": "use-cache", + "type": "option", + "char": "c", + "description": "use cached data", + "aliases": ["useCache", "cache"] + } + }, + "args": [] + } + } +}