UNPKG

@oclif/plugin-commands

Version:

plugin to show the list of all the commands

140 lines (139 loc) 5.97 kB
import { Command, Flags, toConfiguredId } from '@oclif/core'; import { printTable } from '@oclif/table'; import _ from 'lodash'; // @ts-expect-error because object-treeify does not have types: https://github.com/blackflux/object-treeify/issues/1077 import treeify from 'object-treeify'; const COLUMNS = ['id', 'plugin', 'summary', 'type']; function createTree(commands) { const tree = {}; for (const command of commands) { const parts = command.id.split(':'); let current = tree; for (const part of parts) { current[part] = current[part] || {}; current = current[part]; } } return tree; } function mergePrototype(result, command) { const proto = Object.getPrototypeOf(command); const filteredProto = _.pickBy(proto, (v) => v !== undefined); return Object.keys(proto).length > 0 ? mergePrototype({ ...filteredProto, ...result }, proto) : result; } export default class Commands extends Command { static description = 'List all <%= config.bin %> commands.'; static enableJsonFlag = true; static flags = { columns: Flags.custom({ char: 'c', delimiter: ',', description: 'Only show provided columns (comma-separated).', exclusive: ['tree'], multiple: true, options: COLUMNS, })(), deprecated: Flags.boolean({ description: 'Show deprecated commands.' }), extended: Flags.boolean({ char: 'x', description: 'Show extra columns.', exclusive: ['tree'] }), hidden: Flags.boolean({ description: 'Show hidden commands.' }), 'no-truncate': Flags.boolean({ description: 'Do not truncate output.', exclusive: ['tree'] }), sort: Flags.option({ default: 'id', description: 'Property to sort by.', exclusive: ['tree'], options: COLUMNS, })(), tree: Flags.boolean({ description: 'Show tree of commands.' }), }; async run() { const { flags } = await this.parse(Commands); let commands = this.getCommands(); if (!flags.hidden) { commands = commands.filter((c) => !c.hidden); } if (!flags.deprecated) { const deprecatedAliases = new Set(commands.filter((c) => c.deprecateAliases).flatMap((c) => c.aliases)); commands = commands.filter((c) => c.state !== 'deprecated' && !deprecatedAliases.has(c.id)); } const { config } = this; commands = _.sortBy(commands, flags.sort).map((command) => // Template supported fields. ({ ...command, description: (typeof command.description === 'string' && _.template(command.description)({ command, config })) || undefined, summary: (typeof command.summary === 'string' && _.template(command.summary)({ command, config })) || undefined, usage: (typeof command.usage === 'string' && _.template(command.usage)({ command, config })) || undefined, })); if (flags.tree) { const tree = createTree(commands); this.log(treeify(tree)); } else if (!this.jsonEnabled()) { printTable({ borderStyle: 'vertical-with-outline', columns: (flags.columns ?? ['id', 'summary', ...(flags.extended ? ['plugin', 'type'] : [])]), data: commands.map((c) => ({ id: toConfiguredId(c.id, config), plugin: c.pluginName, summary: c.summary ?? c.description, type: c.pluginType, })), headerOptions: { formatter: 'capitalCase', }, overflow: flags['no-truncate'] ? 'wrap' : 'truncate', sort: { [flags.sort]: 'asc' }, }); } const json = _.uniqBy(await Promise.all(commands.map(async (cmd) => { let commandClass; try { commandClass = await cmd.load(); } catch (error) { this.debug(error); return cmd; } const obj = { ...mergePrototype(commandClass, commandClass), ...cmd }; // The plugin property on the loaded class contains a LOT of information including all the commands again. Remove it. delete obj.plugin; // If Command classes have circular references, don't break the commands command. return this.removeCycles(obj); })), 'id'); return json; } getCommands() { return this.config.commands; } removeCycles(object) { // Keep track of seen objects. const seenObjects = new WeakMap(); const _removeCycles = (obj) => { // Use object prototype to get around type and null checks if (Object.prototype.toString.call(obj) === '[object Object]') { // We know it is a "Dictionary" because of the conditional const dictionary = obj; // Seen, return undefined to remove. if (seenObjects.has(dictionary)) return; seenObjects.set(dictionary, undefined); for (const key in dictionary) { // Delete the duplicate object if cycle found. if (_removeCycles(dictionary[key]) === undefined) { delete dictionary[key]; } } } else if (Array.isArray(obj)) { for (const i in obj) { if (_removeCycles(obj[i]) === undefined) { // We don't want to delete the array, but we can replace the element with null. obj[i] = null; } } } return obj; }; return _removeCycles(object); } }