UNPKG

@ideascol/cli-maker

Version:

A simple library to help create CLIs

355 lines (354 loc) 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CLI = void 0; const readline_1 = __importDefault(require("readline")); const colors_1 = require("../colors"); const interfaces_1 = require("../interfaces"); const validator_1 = require("./validator"); class CLI { constructor(name, description, options) { this.name = name; this.description = description; this.options = options; this.commands = []; if (this.options == null) { this.options = { interactive: true, version: '1.0.0' }; } this.validator = new validator_1.Validator(); } getCommands() { return this.commands; } getName() { return this.name; } getDescription() { return this.description; } getOptions() { return this.options; } command(command) { this.commands.push(command); } setOptions(options) { this.options = options; } parse(argv) { const [nodePath, scriptPath, ...args] = argv; if (args.length === 0 || args[0] === '--version') { if (args[0] === '--version') { console.log(`\n${colors_1.Colors.FgGreen}${this.name} version: ${this.options?.version}${colors_1.Colors.Reset}\n`); } else { this.help(); } return; } // Check if we're dealing with a command that has subcommands const commandPath = []; let currentArgs = [...args]; let i = 0; // Build the command path (e.g., "create", "create project", etc.) while (i < args.length) { const potentialCommand = args[i]; if (potentialCommand.startsWith('--')) break; commandPath.push(potentialCommand); currentArgs = args.slice(i + 1); i++; // Check if we've found a valid command path if (commandPath.length === 1) { const rootCommand = this.findCommand(commandPath[0]); if (!rootCommand) { this.showUnknownCommandError(commandPath[0]); return; } // If no subcommands or we've reached the end of args, stop here if (!rootCommand.subcommands || i >= args.length || args[i].startsWith('--')) break; } else { // We're looking for a subcommand const result = this.findSubcommand(commandPath); if (!result) { // If not found, back up one level and treat the rest as arguments commandPath.pop(); currentArgs = args.slice(i); break; } // If no further subcommands or we've reached the end of args, stop here const { command } = result; if (!command.subcommands || i >= args.length || args[i].startsWith('--')) break; } } // Handle help flag for any command level if (currentArgs.includes('--help')) { if (commandPath.length === 1) { const command = this.findCommand(commandPath[0]); if (command) this.commandHelp(command); } else if (commandPath.length > 1) { const result = this.findSubcommand(commandPath); if (result) this.commandHelp(result.command); } return; } // Execute the command or subcommand let commandToExecute; if (commandPath.length === 1) { commandToExecute = this.findCommand(commandPath[0]); } else if (commandPath.length > 1) { const result = this.findSubcommand(commandPath); if (result) commandToExecute = result.command; } if (!commandToExecute) { this.showUnknownCommandError(commandPath.join(' ')); return; } const params = this.parseArgs(currentArgs, commandToExecute); if (params.error) { console.log(`\n${params.error}`); process.exit(1); } if (Object.keys(params.result).length > 0 && this.options.interactive) { this.options.interactive = false; } const missingParams = this.getMissingParams(commandToExecute, params.result); if (missingParams.length > 0 && this.options?.interactive) { this.handleMissingParams(commandToExecute.params, params.result, commandToExecute); } else { if (missingParams.length > 0) { console.log(`\n${colors_1.Colors.FgRed}Error: missing params${colors_1.Colors.Reset}\n`); missingParams.map((param) => { console.log(`> ${colors_1.Colors.FgRed}${param.name}${colors_1.Colors.Reset}`); console.log(` > ${colors_1.Colors.FgGray}Type: ${param.type}${colors_1.Colors.Reset}`); console.log(` > ${colors_1.Colors.FgGray}Description: ${param.description}${colors_1.Colors.Reset}`); if (param.options) { console.log(` > ${colors_1.Colors.FgGray}Options: ${param.options}${colors_1.Colors.Reset}`); } }); console.log(`\n${colors_1.Colors.FgYellow}Optional missing params${colors_1.Colors.Reset}\n`); this.getOptionalParams(commandToExecute, params.result).map((param) => { console.log(`> ${colors_1.Colors.FgYellow}${param.name}${colors_1.Colors.Reset}`); console.log(` > ${colors_1.Colors.FgGray}Type: ${param.type}${colors_1.Colors.Reset}`); console.log(` > ${colors_1.Colors.FgGray}Description: ${param.description}${colors_1.Colors.Reset}`); if (param.options) { console.log(` > ${colors_1.Colors.FgGray}Options: ${param.options}${colors_1.Colors.Reset}`); } }); process.exit(1); } commandToExecute.action(params.result); } } help() { console.log(""); console.log(`${colors_1.Colors.Bright}Welcome to ${colors_1.Colors.FgGreen}${this.name}${colors_1.Colors.Reset}`); console.log(""); console.log(`${colors_1.Colors.FgYellow}${this.description}${colors_1.Colors.Reset}`); console.log(""); console.log(`${colors_1.Colors.Bright}Available commands:${colors_1.Colors.Reset}`); this.commands.forEach(cmd => { console.log(`${colors_1.Colors.FgGreen} ${cmd.name}${colors_1.Colors.Reset}: ${cmd.description}`); if (cmd.subcommands && cmd.subcommands.length > 0) { cmd.subcommands.forEach(subcmd => { console.log(`${colors_1.Colors.FgGreen} ${cmd.name} ${subcmd.name}${colors_1.Colors.Reset}: ${subcmd.description}`); }); } }); console.log(""); } commandHelp(command) { console.log(`${colors_1.Colors.Bright}Help for command: ${colors_1.Colors.FgGreen}${command.name}${colors_1.Colors.Reset}`); console.log(`${colors_1.Colors.FgGreen}Description:${colors_1.Colors.Reset} ${command.description}`); if (command.params.length > 0) { console.log(`${colors_1.Colors.FgGreen}Parameters:${colors_1.Colors.Reset}`); command.params.forEach(param => { console.log(`${colors_1.Colors.FgYellow}(${param.type}) ${colors_1.Colors.Reset}${colors_1.Colors.FgGreen}${param.name}${colors_1.Colors.Reset}: ${param.description} ${param.required ? '(required)' : ''}`); }); } if (command.subcommands && command.subcommands.length > 0) { console.log(`${colors_1.Colors.FgGreen}Subcommands:${colors_1.Colors.Reset}`); command.subcommands.forEach(subcmd => { console.log(`${colors_1.Colors.FgGreen} ${subcmd.name}${colors_1.Colors.Reset}: ${subcmd.description}`); }); } } findCommand(commandName, commands = this.commands) { return commands.find(cmd => cmd.name === commandName); } findSubcommand(commandPath) { if (commandPath.length < 2) return undefined; const rootCommandName = commandPath[0]; const rootCommand = this.findCommand(rootCommandName); if (!rootCommand || !rootCommand.subcommands) return undefined; let currentCommand = rootCommand; let currentCommands = rootCommand.subcommands; let i = 1; while (i < commandPath.length - 1) { const subCmd = this.findCommand(commandPath[i], currentCommands); if (!subCmd || !subCmd.subcommands) return undefined; currentCommand = subCmd; currentCommands = subCmd.subcommands; i++; } const finalSubcommand = this.findCommand(commandPath[commandPath.length - 1], currentCommands); if (!finalSubcommand) return undefined; return { parentCommand: currentCommand, command: finalSubcommand }; } showUnknownCommandError(commandName) { console.log(`${colors_1.Colors.FgRed}Unknown command: ${commandName}${colors_1.Colors.Reset}`); this.help(); } getMissingParams(command, result) { const requiredParams = command.params.filter(p => p.required === true); return requiredParams.filter(p => result[p.name] === undefined); } getOptionalParams(command, result) { const optionalParams = command.params.filter(p => p.required === false || p.required === undefined); return optionalParams.filter(p => result[p.name] === undefined); } handleMissingParams(missingParams, result, command) { if (this.options.interactive) { this.promptForMissingParams(missingParams, result).then(fullParams => { command.action(fullParams); }); } else { console.log(`${colors_1.Colors.FgRed}Missing required parameters:${colors_1.Colors.Reset} ${missingParams.map(p => p.name).join(', ')}`); process.exit(1); } } parseArgs(args, command) { const result = {}; for (const arg of args) { const [key, value] = arg.split('='); if (key.startsWith('--')) { const paramName = key.slice(2); const commandParam = command.params.find(p => p.name === paramName); if (commandParam) { const validation = this.validateParam(value, commandParam.type, commandParam.required, commandParam.options, paramName); if (validation.error) { return { error: validation.error }; } result[paramName] = validation.value; } else { return { error: `\nParam ${colors_1.Colors.FgRed}${paramName}${colors_1.Colors.Reset} ${colors_1.Colors.Bright}is not allowed${colors_1.Colors.Reset}` }; } } } return { result }; } validateParam(value, type, isRequired, options, paramName) { return this.validator.validateParam(value, type, isRequired, options, paramName); } findParamType(paramName) { return this.commands.flatMap(command => command.params).find(param => param.name === paramName); } async promptForMissingParams(missingParams, existingParams) { const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout }); const askQuestion = (question) => { return new Promise(resolve => rl.question(question, resolve)); }; const prompts = missingParams.reduce((promise, param) => { return promise.then(async (answers) => { let answer; let validation; if (param.type === interfaces_1.ParamType.List && param.options) { const isRequired = param.required ? '(required) ' : ''; console.log(`\n${colors_1.Colors.FgYellow}(${param.type}) ${colors_1.Colors.Reset}${colors_1.Colors.FgGreen}${param.name}${colors_1.Colors.Reset} `); console.log(`${colors_1.Colors.FgYellow}> ${colors_1.Colors.Reset}${colors_1.Colors.FgGray}${isRequired}${param.description}:${colors_1.Colors.Reset}\n`); answer = await this.promptWithArrows(param); validation = { value: param.options[parseInt(answer, 10)] }; } else { do { const isRequired = param.required ? '(required) ' : '(Enter to skip) '; const message = `\n${colors_1.Colors.FgYellow}(${param.type}) ${colors_1.Colors.Reset}${colors_1.Colors.FgGreen}${param.name}${colors_1.Colors.Reset}\n${colors_1.Colors.FgYellow}> ${colors_1.Colors.Reset}${colors_1.Colors.FgGray}${isRequired}${param.description}:${colors_1.Colors.Reset}\n`; answer = await askQuestion(message); validation = this.validateParam(answer, param.type, param.required, param.options); if (validation.error) { console.log(validation.error); } } while (validation.error); } return { ...answers, [param.name]: validation.value }; }); }, Promise.resolve(existingParams)); return prompts.finally(() => rl.close()); } async promptWithArrows(param) { return new Promise(resolve => { let index = 0; const options = param.options; const renderOptions = () => { options.forEach((option, i) => { process.stdout.write('\x1B[2K\x1B[0G'); if (i === index) { process.stdout.write(`${colors_1.Colors.FgGreen}> ${option}${colors_1.Colors.Reset}\n`); } else { process.stdout.write(` ${option}\n`); } }); process.stdout.write(`\x1B[${options.length}A`); }; const clearLines = (numLines) => { for (let i = 0; i < numLines; i++) { process.stdout.write('\x1B[2K\x1B[1A'); } process.stdout.write('\x1B[2K\x1B[0G'); }; const keypressHandler = (str, key) => { if (key.name === 'up') { index = (index > 0) ? index - 1 : options.length - 1; renderOptions(); } else if (key.name === 'down') { index = (index + 1) % options.length; renderOptions(); } else if (key.name === 'return') { process.stdin.removeListener('keypress', keypressHandler); clearLines(options.length); options.forEach((option, i) => { if (i === index) { process.stdout.write(`${colors_1.Colors.FgGreen}> ${option}${colors_1.Colors.Reset}\n`); } else { process.stdout.write(` ${option}\n`); } }); process.stdout.write(`\nSelected: ${options[index]}\n`); resolve(index.toString()); return; } }; readline_1.default.emitKeypressEvents(process.stdin); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.on('keypress', keypressHandler); renderOptions(); }); } } exports.CLI = CLI;