UNPKG

erm-cli

Version:

Reekoh CLI - command line tool for publishing reekoh plugins.

469 lines (399 loc) 12.3 kB
'use strict' let os = require('os') let _ = require('lodash') let request = require('request') let path = require('path') let async = require('async') let yaml = require('js-yaml') let archiver = require('archiver') let getDirSize = require('get-folder-size') let fs = Promise.promisifyAll(require('fs')) let simpleEncryptor = require('simple-encryptor') let cliff = require('cliff') let si = require('systeminformation') let CliError = require('./lib/cli-error') let prompt = require('prompt-sync')({ history: false, sigint: true }) const userCredentials = path.join(os.tmpdir(), 'reekoh-credentials') const config = require('./config.json') const packageJson = require('./package.json') class Service { handleUnknownCommand (command) { console.log(`${packageJson.name}: '${command}' is not a ${packageJson.name} command. \nSee '${packageJson.name} --help'`) } getEncryptionKey () { return new Promise((resolve) => { si.system().then(data => { let {serial, uuid} = data let encKey = `${serial}-${uuid}` encKey = (encKey !== '---') ? encKey : config.encKey resolve(encKey) }).catch(() => { resolve(config.encKey) }) }) } encrypt (data = '') { return this.getEncryptionKey().then(encKey => { let encryptor = simpleEncryptor(encKey) return Promise.resolve(encryptor.encrypt(data)) }) } decrypt (data = '') { return this.getEncryptionKey().then(encKey => { let encryptor = simpleEncryptor(encKey) return Promise.resolve(encryptor.decrypt(data)) }) } getUserCredentials (cmdOptions) { return new Promise((resolve, reject) => { let credentials = {} let fields = [{ name: 'Username' }, { name: 'Password', promptProps: { echo: '*' } }] async.eachLimit(fields, 1, (field, cb) => { let key = field.name.toLowerCase() if (cmdOptions.hasOwnProperty(key)) { credentials[key] = cmdOptions[key] return cb() } credentials[key] = prompt(`${field.name}: `, field.promptProps) if (_.isEmpty(credentials[key])) { return cb(new CliError(`${field.name} is required`)) } cb() }, (error) => { if (error) { if (error.message === 'canceled') { return resolve({}) } reject(error) } else { resolve(credentials) } }) }) } userAuth (credentials) { return new Promise((resolve, reject) => { request({ method: 'post', url: `${config.reekohApi}/users/auth`, body: credentials, headers: { 'Accept-Version': '1.0.0', 'Accept': 'application/json' }, json: true }, (error, res, body) => { if (error) { return reject(error) } if (res.statusCode !== 200) { error = new CliError(body.message, body) error.detail = body return reject(error) } resolve(body) }) }) } switchRole (credentials, token) { return new Promise((resolve, reject) => { request({ method: 'post', url: `${config.reekohApi}/users/switch-role`, body: credentials, headers: { 'Accept-Version': '1.0.0', 'Accept': 'application/json', 'Authorization': `Bearer ${token}` }, json: true }, (error, res, body) => { if (error) { return reject(error) } if (res.statusCode !== 200) { error = new CliError(body.message, body) error.detail = body return reject(error) } resolve(body) }) }) } getUserRoles (token) { return new Promise((resolve, reject) => { request({ method: 'get', url: `${config.reekohApi}/users/roles`, headers: { 'Accept': 'application/json', 'Accept-Version': '1.0.0', 'Authorization': `Bearer ${token}` }, json: true }, (error, res, body) => { if (error) { return reject(error) } else if (res.statusCode !== 200) { error = new CliError(body.message, body) error.detail = body return reject(error) } resolve(body) }) }) } selectUserRole (userRoles) { return new Promise((resolve, reject) => { if (_.isEmpty(userRoles)) { return reject(new CliError(`Sorry, you don't have any role with write permission.`)) } let error let field = 'accountRole' let validOptions = [] let roleOptions = userRoles.map((userRole, i) => { let optNumber = `${i + 1}` validOptions.push(optNumber) return [`${optNumber})`, userRole.account.name, userRole.role.name] }) roleOptions.unshift(['', 'Account', 'Role']) console.log('\n' + cliff.stringifyRows(roleOptions, ['yellow']) + '\n') let selectedOption = prompt('Account role: ') if (_.isEmpty(selectedOption)) { error = new CliError(`Account role is required`) } else if (validOptions.indexOf(`${selectedOption}`) === -1) { error = new CliError('Invalid option selected.') } if (error) { reject(error) } selectedOption -= 1 resolve(userRoles[selectedOption]) }) } saveUserData (data) { return this.encrypt(data).then(encryptedData => { return fs.writeFileAsync(userCredentials, encryptedData) }).then(() => { return Promise.resolve(data) }) } getUserData () { return new Promise((resolve, reject) => { fs.readFile(userCredentials, 'utf8', (error, data) => { let errorMessage = 'Not logged in.' if (error) { if (error.code === 'ENOENT') { error = new CliError(errorMessage) } return reject(error) } this.decrypt(data).then(decryptedData => { if (_.isEmpty(decryptedData)) { return reject(new CliError(errorMessage)) } resolve(decryptedData) }).catch(reject) }) }) } removeUserData () { return new Promise((resolve, reject) => { fs.unlink(userCredentials, (error) => { if (error) { if (error.code === 'ENOENT') { return reject(new CliError('No logged in user found.')) } return reject(error) } resolve() }) }) } getPluginDetails (pathToManifest) { return new Promise((resolve, reject) => { fs.readFileAsync(pathToManifest, 'utf8').then(content => { let jsonContent = yaml.safeLoad(content) resolve(jsonContent) }).catch(error => { if (error.code === 'ENOENT') { error = new CliError(`Unable to locate manifest file.`) } if (error.name === 'YAMLException') { error = new CliError(`Invalid manifest file : ${error.message}`) } reject(error) }) }) } validateFile (file, plugin) { return new Promise((resolve, reject) => { let errorMsg = `Invalid manifest detail on '${file.key}'. ` let filePath = _.get(plugin, file.key, null) if (_.isNil(filePath)) { return resolve(plugin) } filePath = path.resolve(file.manifestDir, filePath) let fileExtension = path.extname(filePath).replace('.', '') if (!_.includes(file.validExtensions, fileExtension)) { reject(new CliError(`${errorMsg} Invalid file type. Must be one of the following format [${file.validExtensions.join(', ')}]`)) } fs.readFileAsync(filePath, 'utf8').then(content => { if (file.getContent) { _.set(plugin, file.key, content) } resolve(plugin) }).catch(error => { reject(new CliError(`${errorMsg} ${error.message}`)) }) }) } saveIcon (options, plugin) { return new Promise((resolve, reject) => { let iconPath = _.get(plugin, 'metadata.icon') if (_.isEmpty(iconPath)) { return resolve(plugin) } iconPath = path.resolve(options.manifestDir, iconPath) let requestOptions = { method: 'post', url: `${config.reekohApi}/files`, formData: { type: 'plugin-icon', file: fs.createReadStream(iconPath) }, headers: { Authorization: `Bearer ${options.token}` }, json: true } request(requestOptions, (error, res, body) => { if (error) { reject(error) } else if (res.statusCode !== 201) { reject(new CliError(body.message)) } else { _.set(plugin, 'metadata.icon', body._id) resolve(plugin) } }) }) } savePluginCode (options, plugin) { return new Promise((resolve, reject) => { let pluginRootDir = _.get(plugin, 'metadata.release.pluginRootDir') if (_.isEmpty(pluginRootDir)) { return reject(new CliError(`Missing plugin root directory.`)) } pluginRootDir = path.resolve(options.manifestDir, pluginRootDir) let createZipFile = Promise.promisify((dirPath, cb) => { let zipFile = new Date().getTime() zipFile = path.join(os.tmpdir(), `reekoh-${zipFile}.zip`) let archive = archiver('zip') let output = fs.createWriteStream(zipFile) archive.on('error', cb) output.on('close', () => { cb(null, zipFile) }) archive.pipe(output) archive.glob('!(node_modules)') archive.finalize() }) let saveZipFile = Promise.promisify(pathToZip => { let requestOptions = { method: 'post', url: `${config.reekohApi}/files`, formData: { type: 'plugin-code', file: fs.createReadStream(pathToZip) }, headers: { Authorization: `Bearer ${options.token}` }, json: true } request(requestOptions, (error, res, body) => { if (error) { reject(error) } else if (res.statusCode !== 201) { error = new CliError(body.message, body) error.detail = body return reject(error) } else { _.set(plugin, 'metadata.release.code', body._id) resolve(plugin) } }) }) let getPluginSize = Promise.promisify(getDirSize) fs.readdirAsync(pluginRootDir, 'utf8').then(() => { return getPluginSize(pluginRootDir) }).then((size) => { if (size > config.maxZipSize) { return reject(new CliError('The maximum allowable plugin size is 5 MB.')) } return createZipFile(pluginRootDir) }).then((pathToZip) => { return saveZipFile(pathToZip) }).then((zipFile) => { _.set(plugin, 'metadata.release.code', zipFile._id) resolve(plugin) }).catch(error => { reject(new CliError(`${error.message}`)) }) }) } submitPlugin (token, method, plugin) { return new Promise((resolve, reject) => { let requestOptions = { method, url: `${config.reekohApi}/plugins`, body: plugin.metadata, headers: { Authorization: `Bearer ${token}` }, json: true } request(requestOptions, (error, res, body) => { if (error) { return reject(error) } else if (!_.includes([200, 201], res.statusCode)) { error = new CliError(body.message, body) error.detail = body return reject(error) } else { resolve(plugin) } }) }) } handleError (error) { if (error instanceof CliError) { console.error(`Error: ${error.message}`) let eData = error.data if (!_.isEmpty(eData.details)) { eData.details.forEach((detail) => { console.log(` - ${detail.msg}`) }) } } else { console.error(config.genericErrorMessage) } process.exit() } processExit () { console.log('\n') process.exit() } } module.exports = new Service()