UNPKG

@nimbella/commander-cli

Version:

Commander CLI is a Nimbella Commander development tool that allows you to create, run & publish your serverless functions as commands that can run in Slack, Microsoft Teams, and Mattermost.

278 lines (231 loc) 7.5 kB
// Copyright (c) 2020-present, Nimbella, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. function generateReadme(commandSetName, commandsYamlPath) { const fs = require('fs'); const yaml = require('js-yaml'); const file = commandsYamlPath; const { commands } = yaml.safeLoad(fs.readFileSync(file, 'utf8')); const title = `# ${commandSetName} Command Set\n\n`; const commandsSection = ['## Commands\n']; const usageSection = ['## Usage\n']; for (const [command, commandDetails] of Object.entries(commands).sort()) { let cmdRep = `/nc ${command}`; if (commandDetails.parameters) { for (const parameter of commandDetails.parameters) { if (parameter.optional) { cmdRep += ` [<${parameter.name}>]`; } else { cmdRep += ` <${parameter.name}>`; } } } if (commandDetails.options) { for (const option of commandDetails.options) { cmdRep += ` [-${option.name} <${ option.value ? option.value : option.name }>]`; } } commandsSection.push(`- [\`${command}\`](#${command})`); usageSection.push(`### \`${command}\`\n`); if (commandDetails.description) { usageSection.push(commandDetails.description); } usageSection.push(`\`\`\`sh\n${cmdRep}\n\`\`\`\n`); } return title + commandsSection.join('\n') + '\n\n' + usageSection.join('\n'); } function parseCommandProperties(cmd) { const command = { name: '', options: [], parameters: [] }; command.name = cmd.trim().split(' ').shift(); // This is to help regex when the last substring is a parameter. cmd += ' '; // Matches [-flag] or [-flag <value>] const options = cmd.match(/\[-([\w\d]+)\]|\[-([\w\d]+)(?:\s+\<([^\]]+))?\>]/g) || []; // Matches [<optional>] const optionalParameters = cmd.match(/\[\<([\w\d]+)\>\]/g) || []; // Matches <parameter> const parameters = cmd.match(/\<([\w\d]+)\>[^\]]/g) || []; for (const option of options) { const flagValues = option.trim().split(' '); const name = flagValues.length === 2 ? flagValues[0].slice(2) // Remove [- : flagValues[0].slice(2, -1); // Remove [-] const value = flagValues.length === 2 ? flagValues[1].slice(1, -2) // Remove <>] : name; command.options.push({ name, value, }); } for (const parameter of parameters) { command.parameters.push({ name: parameter.trim().slice(1, -1), // Remove <> optional: false, }); } for (const optionalParameter of optionalParameters) { command.parameters.push({ name: optionalParameter.trim().slice(2, -2), // Remove [<>] optional: true, }); } return command; } const askQuestions = async () => { const { prompt } = require('inquirer'); const commandSet = { name: '', commands: [], }; const { name } = await prompt([ { type: 'input', name: 'name', message: 'What is the name of your command set?', }, ]); commandSet.name = name; const { numberOfCommands } = await prompt([ { type: 'number', name: 'numberOfCommands', default: 1, message: 'How many commands do you want in your command set?', }, ]); const { language } = await prompt([ { type: 'list', name: 'language', default: 'NodeJS', message: `Select language for ${name}`, choices: ['NodeJS', 'Python', 'Go'], }, ]); commandSet.language = language; for (let i = 1; i <= numberOfCommands; i++) { const { commandDefinition } = await prompt([ { type: 'input', default: `hello${i} <name>`, message: `Define your command ${i}:`, name: 'commandDefinition', validate: input => { return new Promise((resolve, reject) => { const secondWord = input.split(' ')[1]; // Test if the second word contains < or [ which indicates that it is an option if (secondWord && !/\<|\[/.test(secondWord)) { reject('spaces are not allowed in command names.'); } resolve(true); }); }, }, ]); const commandName = commandDefinition.trim().split(' ')[0]; const { commandDescription = '' } = await prompt([ { type: 'input', message: `Provide a small description for ${commandName}:`, name: 'commandDescription', }, ]); commandSet.commands.push({ description: commandDescription, ...parseCommandProperties(commandDefinition), }); } return commandSet; }; const getExtension = language => { if (language === 'Python') { return 'py'; } else if (language === 'Go') { return 'go'; } else { return 'js'; } }; const createCommandSet = async commandSet => { const { writeFile, readFile } = require('fs').promises; const { join } = require('path'); const mkdirp = require('mkdirp'); const yaml = require('js-yaml'); const commandSetDir = join(process.cwd(), commandSet.name); const commandsDir = join(commandSetDir, `/packages/${commandSet.name}`); // Create directories. await mkdirp(commandsDir); const commands = {}; const languageExtension = getExtension(commandSet.language); const commandCode = await readFile( join(__dirname, `templates/CommandSource.${languageExtension}.txt`) ); for (const command of commandSet.commands) { commands[command.name] = command; // Remove irrelavent fields if (commands[command.name].options.length === 0) { delete commands[command.name].options; } if (commands[command.name].parameters.length === 0) { delete commands[command.name].parameters; } // Create commands with template code. await writeFile( join(commandsDir, `${command.name}.${languageExtension}`), commandCode ); // The primary reason to remove .name is because we use the same object to generate Yaml file and // we don't want a name field under command. delete commands[command.name].name; } const commandsYamlPath = join(commandSetDir, 'commands.yaml'); // Create commands.yaml file. const commandsYaml = yaml.safeDump({ commands, }); await writeFile(commandsYamlPath, commandsYaml); // Create readme.md file. const readmePath = join(commandSetDir, 'README.md'); await writeFile( readmePath, generateReadme(commandSet.name, commandsYamlPath) ); }; module.exports = async (args = []) => { if (args.length === 0) { return { text: `Run \`command_set create\` to create a new command set.`, }; } if (args[0] === 'create') { const path = require('path'); const commandSet = await askQuestions(); await createCommandSet(commandSet); return { text: `Command set created at ${path.join( process.cwd(), commandSet.name )}`, }; } else { return { text: `Unknown argument: \`${args[0]}\``, }; } };