UNPKG

@gutenye/commander-completion-carapace

Version:

Effortlessly add intelligent autocomplete support to your Commander.js CLI app using Carapace. Supports Bash, Zsh, Fish, Nushell and more

153 lines (132 loc) 3.77 kB
import { merge } from 'lodash-es' import invariant from 'tiny-invariant' import * as yaml from 'yaml' import { CARAPACE_SPECS_DIR } from '#/constants' import type { Carapace, NewCommand } from '#/types/index' import { fs, logger, mergeWithoutNull, question } from '#/utils/index' export async function installCompletion(program: NewCommand) { if (!program._enableCompletion) { return } const { spec, text } = buildSpecText(program) if (!text) { return } const path = `${CARAPACE_SPECS_DIR}/${spec.name}.yaml` const content = await fs.inputFile(path, 'utf8') if (content) { if (content === text) { return } if (!program._enableCompletion.overwrite) { const answer = await question( `Overwrite completion file '${path}'? [y/n] `, ) const newAnswer = answer.trim().toLowerCase() if (newAnswer !== 'y') { return } } } await fs.outputFile(path, text) logger.log(`\nCompletion file is installed to '${path}'\n`) } function buildRootSpec(rootCommand: NewCommand): Carapace.Spec { const rootSpec = buildSpec(rootCommand) invariant(rootSpec, 'rootSpec is undefined') const defaultCommand = rootCommand._findCommand( rootCommand._defaultCommandName, ) if (defaultCommand) { const defaultSpec = buildSpec(defaultCommand, { force: true }) if (defaultSpec?.flags) { rootSpec.flags = rootSpec.flags || {} merge(rootSpec.flags, defaultSpec.flags) } if (defaultSpec?.completion) { rootSpec.completion = rootSpec.completion || {} merge(rootSpec.completion, defaultSpec.completion) } } return rootSpec } // I'm recursive function buildSpec( command: NewCommand, { force }: BuildSpecOptions = {}, ): Carapace.Spec | undefined { if (command._hidden && !force) { return } const spec: Carapace.Spec = { name: command._name, } if (command._description) { spec.description = command._description } if (command._aliases.length > 0) { spec.aliases = command._aliases } const completion: Carapace.Completion = {} const positional = [] for (const argument of command.registeredArguments) { positional.push(argument.argChoices || []) } if (positional.some((v) => v.length > 0)) { completion.positional = positional } for (const option of command.options) { spec.flags = spec.flags || {} let flag = [option.short, option.long].filter(Boolean).join(', ') if (!option.isBoolean) { flag += '=' } if (option.required) { flag += '=!' } if (option.optional) { flag += '=?' } spec.flags[flag] = option.description if (option.argChoices) { completion.flag = completion.flag || {} completion.flag[option.name()] = option.argChoices } } if (Object.keys(completion).length > 0) { spec.completion = completion } if (command._completion) { spec.completion = spec.completion || {} mergeWithoutNull(spec.completion, command._completion) } if (command._carapace) { mergeWithoutNull(spec, command._carapace) } for (const subcommand of command.commands) { const newSpec = buildSpec(subcommand) if (newSpec) { spec.commands = spec.commands || [] spec.commands.push(newSpec) } } return spec } export function buildSpecText(command: NewCommand) { const spec = buildRootSpec(command) if ( !(spec.commands || spec.persistentflags || spec.flags || spec.completion) ) { return {} } if (!command._name) { throw new Error( '[completion.buildSpecText] command name is missing, use program.name() to define it', ) } const text = yaml.stringify(spec) return { spec, text } } type BuildSpecOptions = { force?: boolean }