@ideascol/cli-maker
Version:
A simple library to help create CLIs
355 lines (354 loc) • 16.4 kB
JavaScript
;
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;