Source: index.js

const {Client, TextChannel} = require('discord.js');

/**
 * Options for a command
 * @typedef {Object} CommandOptions
 * @property {number} [minArgs=0] Minimum arguments required to trigger the event
 * @property {number} [maxArgs=-1] Maximum arguments allowed to trigger the event
 * @property {boolean} [displayInHelp=true] If false, the command will not be displayed in the help
 * @property {string} [helpMessage="No help available"] Contains the help message displayed when user uses help command
 * @property {UsageMessage} [usageMessage=""] Contains the message displayed when the command is badly used.
 * @property {PermissionResolvable} [requiredPermission=0] Store the permission required to use the command (Will be ignored in DM/Group DM)
 * @property {boolean} [dmAllowed=false] If false, the event isn't triggered when the command is sent by PM
 */
const DefaultCommandOptions = {
    minArgs: 0,
    maxArgs: -1,
    displayInHelp: true,
    helpMessage: 'No help available',
    usageMessage: '',
    requiredPermission: 0,
    dmAllowed: false,
};


/**
 * The callback function called when a valid command is typed
 * @typedef {function} CommandCallback
 * @param {Message} [message] Discord.js Message sent by user
 * @param {string} [commandName] Command name, without the prefix
 * @param {string[]} [args] Arguments passed to the command, sliced following spaces
 */

/**
 * The usage message. It's a string, but you can type %p, which will be replaced by the prefix, %c, replaced by the command name (without prefix), and %f, replaced by the full command (with prefix)
 * @typedef {string} UsageMessage
 */

/**
 * Represents a bot which supports commands.
 * {@link https://discord.js.org/#/docs/main/stable/class/Client | Discord.js Client class documentation.}
 * @extends Client
 */
class CommandClient extends Client {
    /**
     * Class constructor
     * @param {string} [prefix='!'] Prefix for commands.
     * @param {ClientOptions} [options={}] Options to be passed to the Client.
     * @constructor
     */
    constructor(prefix = '!', options = {}) {
        super(options);
        /**
         * Prefix of the command
         * @type {string}
         */
        this.prefix = prefix;

        /**
         * Set if the help is enabled
         * @type {boolean}
         */
        this.enableHelp = true;

        /**
         * All registered commands
         * @type {Object.<string, CommandInfos>}
         * @private
         */
        this._registeredCommands = [];

        /**
         * Message displayed when usage doesn't have enough privileges to run command.
         * @type {string}
         * @default "You aren't allowed to run this command."
         */
        this.commandNotAllowedMessage = "You aren't allowed to run this command.";

        this.on("message", (message) => {
            if(message.content.startsWith(prefix)) {
                let args = message.content.substr(this.prefix.length).split(' ');
                const commandName = args[0];
                args.shift();

                if(typeof this._registeredCommands[commandName] !== 'undefined') {
                    const commandData = this._registeredCommands[commandName];
                    const commandOptions = commandData.options;
                    const isDm = (message.channel.type === 'dm' || message.channel.type === 'group');

                    if(isDm && !commandOptions.dmAllowed)
                        return;

                    if(args.length < commandOptions.minArgs) {
                        this._displayUsageMessage(message, commandData);
                        return;
                    }

                    if(commandOptions.maxArgs >= 0 && args.length > commandOptions.maxArgs) {
                        this._displayUsageMessage(message, commandData);
                        return;
                    }

                    if(!isDm && commandOptions.requiredPermission !== 0 && (!(message.channel instanceof TextChannel) || !message.channel.permissionsFor(message.author).has(commandOptions.requiredPermission))) {
                        this._replyMessage(message, this.commandNotAllowedMessage);
                        return;
                    }

                    commandData.callback(message, commandName, args);
                }
            }
        });

        this.registerCommand("help", (message) => {
            if(this.enableHelp) {
                const dm = (message.channel.type === 'dm' || message.channel.type === 'group');
                if (dm || !(message.channel instanceof TextChannel) || message.channel.permissionsFor(this.user).has("SEND_MESSAGES")) {
                    let help = '';
                    for(let key in this._registeredCommands) {
                        if(this._registeredCommands.hasOwnProperty(key)) {
                            let commandData = this._registeredCommands[key];
                            if (commandData.options.displayInHelp) {
                                if (!dm || commandData.options.dmAllowed) {
                                    help += `\`${this.prefix}${commandData.name}\`: ${commandData.options.helpMessage}\n`;
                                }
                            }
                        }
                    }
                    if(help !== '') {
                        message.channel.send(help);
                    }
                }
            }

        }, {
            helpMessage: "List all available commands and their usages",
            dmAllowed: true
        })
    }

    /**
     * Register a new command
     * @param {string} command Command without the prefix
     * @param {CommandCallback} callback Callback function called when the command is triggered
     * @param {CommandOptions} [options={}] Options evaluated when the command is triggered
     */
    registerCommand(command, callback, options = {}) {
        let o = {};
        Object.assign(o, DefaultCommandOptions);
        Object.assign(o, options);

        /**
         * Stores the informations about the command
         * @typedef {{name: string, callback: CommandCallback, options: CommandOptions}} CommandInfos
         */
        const commandInfos = {
            name: command,
            callback: callback,
            options: o,
        };

        this._registeredCommands[command] = commandInfos;
    }

    /**
     * Unregisters a command if registered
     * @param {string} command Command to unregister
     */
    unregisterCommand(command) {
        if(typeof this._registeredCommands[command] !== 'undefined') {
            delete this._registeredCommands[command];
        }
    }

    /**
     * Edit command datas (callback, and options, if specified)
     * @param {string} command The command to edit
     * @param {CommandCallback} callback The new callback function
     * @param {CommandOptions} [options={}] The options to edit (if an existing option isn't set, its value will be unchanged)
     */
    editCommandData(command,callback,options = {}) {
        if(typeof this._registeredCommands[command] !== 'undefined') {
            this._registeredCommands[command].callback = callback;
            Object.assign(this._registeredCommands[command].options, options);
        }
    }

    /**
     * Edit command options
     * @param {string} command The command to edit
     * @param {CommandOptions} options The options to edit (if an existing option isn't set, its value will be unchanged)
     */
    editCommandOptions(command, options) {
        if(typeof this._registeredCommands[command] !== 'undefined') {
            Object.assign(this._registeredCommands[command].options, options);
        }
    }

    /**
     * Display the usage message if existing.
     * @param {Message} message the original Discord message
     * @param {CommandInfos} commandInfos
     * @private
     */
    _displayUsageMessage(message, commandInfos) {
        const options = commandInfos.options;
        if(options.usageMessage !== '') {
            let usageMessage = options.usageMessage;
            usageMessage = usageMessage.replace("%p", this.prefix);
            usageMessage = usageMessage.replace("%f", this.prefix + commandInfos.name);
            usageMessage = usageMessage.replace("%c", commandInfos.name);
            this._replyMessage(message, "Usage: `" + usageMessage + "`")
        }
    }

    /**
     * Used to reply to a message, an check to avoid errors
     * @param {Message} originalMessage Original message posted by user
     * @param {string} messageToReply Content of the reply
     * @private
     */
    _replyMessage(originalMessage, messageToReply) {
        if(messageToReply !== '' && (originalMessage.channel.type === 'dm' || originalMessage.channel.type === 'group' || !(originalMessage.channel instanceof TextChannel) || originalMessage.channel.permissionsFor(this.user).has("SEND_MESSAGES"))) {
            originalMessage.reply(messageToReply);
        }
    }

}

exports.CommandClient = CommandClient;