UNPKG

kchatbotbak

Version:

kchatbotbak is base on Botfront.

478 lines (420 loc) 16.1 kB
import path from 'path'; import { promisify } from 'util'; import fs from 'fs'; import axios from 'axios'; import yaml from 'js-yaml'; export const BF_CONFIG_DIR = '.kchatbotbak'; export const BF_CONFIG_FILE = 'kchatbotbak.yml'; export const DOCKER_COMPOSE_TEMPLATE_FILENAME = 'docker-compose-template.yml'; export const DOCKER_COMPOSE_FILENAME = 'docker-compose.yml'; export const DEFAULT_MODEL_FILENAME = 'default_model.tar.gz'; export const ENV_FILENAME = '.env'; import shell from 'shelljs'; import { Docker } from 'docker-cli-js'; import chalk from 'chalk'; import boxen from 'boxen'; import check from 'check-node-version'; import compareVersions from 'compare-version'; export function fixDir(dir) { return dir ? dir : process.cwd(); } export function consoleError(error) { try { console.log(boxen(error)); } catch (e) { console.log(error); } } export function startSpinner(spinner, message) { if (spinner) { spinner.start(message); } else { console.log(message); } } export function setSpinnerText(spinner, message) { if (spinner) { spinner.text = message; } else { console.log(message); } } export function setSpinnerInfo(spinner, message) { if (spinner) { spinner.info = message; } else { console.log(message); } } export function succeedSpinner(spinner, message) { if (spinner) { spinner.succeed(message); } else { console.log(message); } } export function failSpinner(spinner, message, params = {}) { const { exit = true } = params; if (spinner) { spinner.fail(message); } else { console.log(message); } if (exit) process.exit(1); } export function stopSpinner(spinner) { if (spinner) { spinner.stop(); } } export async function getLatestVersion() { try { const response = await axios.get('https://registry.npmjs.org/botfront'); if (response.status === 200) { return response.data['dist-tags'].latest; } // If the call fails we just return the current version return getBotfrontVersion(); } catch (e) { return getBotfrontVersion(); } } export function getProjectVersion() { return getProjectConfig(fixDir(null)).version; } export function getMongoPassword() { return getProjectConfig(fixDir(null)).env.mongo_initdb_root_password; } export function isMinorUpdateWithVersion(projectVersion, botfrontVersion) { const projectMajorVersion = projectVersion.split('.')[1]; const projectMinorVersion = projectVersion.split('.')[2]; const botfrontMajorVersion = botfrontVersion.split('.')[1]; const botfrontMinorVersion = botfrontVersion.split('.')[2]; return ( projectMajorVersion === botfrontMajorVersion && botfrontMinorVersion !== projectMinorVersion ); } export function isMajorUpdateWithVersion(projectVersion, botfrontVersion) { const projectMajorVersion = projectVersion.split('.')[1]; const botfrontMajorVersion = botfrontVersion.split('.')[1]; return compareVersions(botfrontMajorVersion, projectMajorVersion) == 1; } export function isMinorUpdate() { const botfrontVersion = getBotfrontVersion(); const projectVersion = getProjectVersion(); return isMinorUpdateWithVersion(projectVersion, botfrontVersion); } export function isMajorUpdate() { const botfrontVersion = getBotfrontVersion(); const projectVersion = getProjectVersion(); return isMajorUpdateWithVersion(projectVersion, botfrontVersion); } export function shouldUpdateNpmPackageWithVersions(currentVersion, latestVersion) { return compareVersions(latestVersion, currentVersion) == 1; } export async function shouldUpdateNpmPackage() { if (isPrivate()) return false; const currentVersion = getBotfrontVersion(); const latestVersion = await getLatestVersion(); return shouldUpdateNpmPackageWithVersions(currentVersion, latestVersion); } export async function displayNpmUpdateMessage() { const shouldUpdate = await shouldUpdateNpmPackage(); if (shouldUpdate) { const currentVersion = getBotfrontVersion(); const latestVersion = await getLatestVersion(); console.log( boxen( `A new version of kchatbotbak is available: ${chalk.blueBright( currentVersion, )} -> ${chalk.green(latestVersion)}\nRun ${chalk.cyan.bold( 'npm install -g kchatbotbak', )} to update.`, { padding: 1, margin: 1 }, ), ); } return shouldUpdate; } export async function displayProjectUpdateMessage() { if (!isProjectDir()) return; let isMajorUpdate = false; const botfrontVersion = getBotfrontVersion(); const projectVersion = getProjectVersion(); if (isMinorUpdateWithVersion(projectVersion, botfrontVersion)) { console.log( boxen( `Project was made with kchatbotbak ${chalk.blueBright( projectVersion, )} and the currently installed version is ${chalk.green( botfrontVersion, )}\nRun ${chalk.cyan.bold('kchatbotbak update')} to update your project.`, ), ); } if (isMajorUpdateWithVersion(projectVersion, botfrontVersion)) { console.log( boxen( `Project was made with kchatbotbak ${chalk.blueBright( projectVersion, )} and the currently installed version is ${chalk.green( botfrontVersion, )}, which is a major update.\nPlease follow the instructions in the migration guide: ${chalk.cyan.bold( 'https://xxx.com', )}.`, ), ); isMajorUpdate = true; } return isMajorUpdate; } /* Augment the kchatbotbak.yml file with version and project specific values */ export function updateProjectFile({ projectAbsPath, images, env = {}, enableMongoAuth = false, leaveMongoUrl = false, mongoPassword = randomString(), }) { const config = getProjectConfig(projectAbsPath); if (!config.version) { config.version = getBotfrontVersion(); } if (images) { config.images.current = JSON.parse(JSON.stringify(config.images.default)); // deep copy Object.keys(config.images.current).forEach((service) => { if (images[service]) config.images.current[service] = images[service]; }); } config.env = { ...config.env || {}, ...env, }; if (!leaveMongoUrl) { if (enableMongoAuth) { Object.assign(config.env, { mongo_url: `mongodb://root:${mongoPassword}@mongo:27017/bf?authSource=admin`, mongo_initdb_root_username: 'root', mongo_initdb_root_password: mongoPassword, }); } else { Object.assign(config.env, { mongo_url: 'mongodb://mongo:27017/bf', }); } } fs.writeFileSync(getProjectInfoFilePath(projectAbsPath), yaml.safeDump(config)); } export async function updateEnvFile(projectAbsPath) { const config = getProjectConfig(projectAbsPath); let envFileContent = '########################################################################\n'; envFileContent += '# This file is generated when `kchatbotbak up` is invoked. #\n'; envFileContent += '# You can change / add environment variables in .kchatbotbak/kchatbotbak.yml #\n'; envFileContent += '########################################################################\n\n'; if (!config.env) { config.env = {}; } Object.keys(config.env).forEach((variable) => { envFileContent += `${variable.toUpperCase()}=${config.env[variable]}\n`; }); Object.keys(config.images.default).forEach((variable) => { envFileContent += `IMAGES_DEFAULT_${variable.toUpperCase().replace('-', '_')}=${ config.images.default[variable] }\n`; }); Object.keys(config.images.current).forEach((variable) => { envFileContent += `IMAGES_CURRENT_${variable.toUpperCase().replace('-', '_')}=${ config.images.current[variable] }\n`; }); fs.writeFileSync(getProjectEnvFilePath(projectAbsPath), envFileContent); } export const getComposeTemplateFile = dir => getComposeFile(dir, DOCKER_COMPOSE_TEMPLATE_FILENAME); export function generateDockerCompose(exclude = [], dir, projectId) { let initContent = '######################################################################################################\n'; initContent += '# This file is generated when `kchatbotbak up` is invoked. #\n'; initContent += '# Changes in .kchatbotbak/kchatbotbak.yml and .kchatbotbak/docker-compose-template.yml will be reflected here #\n'; initContent += '######################################################################################################\n\n'; const dc = getComposeTemplateFile(dir); exclude.forEach((excl) => { // remove reference to excluded services if (excl in dc.services) delete dc.services[excl]; Object.values(dc.services).forEach((service) => { if ({}.hasOwnProperty.call(service, 'depends_on')) { service.depends_on = service.depends_on.filter( (depend) => depend !== excl, ); if (service.depends_on.length === 0) delete service.depends_on; } }); }); const config = getProjectConfig(dir); const dcCopy = JSON.parse(JSON.stringify(dc)); Object.keys(dc.services) .filter((service) => ['actions', 'rasa'].indexOf(service) < 0) .forEach((service) => { dcCopy.services[service].image = config.images.current[service]; }); if (projectId) { // hot update of projectId ['actions', 'rasa'].forEach(service => { if (!(service in dc.services)) return; dcCopy.services[service].environment = [ ...(dcCopy.services[service].environment || []), `BF_PROJECT_ID=${projectId}`, ]; }) } fs.writeFileSync( getGeneratedComposeFilePath(dir), `${initContent}${yaml.safeDump(dcCopy)}`, ); return true; } export function getProjectConfig(projectAbsPath) { return yaml.safeLoad( fs.readFileSync(getProjectInfoFilePath(projectAbsPath), 'utf-8'), ); } export async function verifySystem() { const docker = new Docker(); const result = await docker.command('info'); // const version = result.object.server_version; if (!result.object) throw `You must install Docker to use kchatbotbak. Please visit ${chalk.green( 'https://www.docker.com/products/docker-desktop', )}`; const results = await promisify(check)({ node: '>= 8.9' }); if (!results.versions.node.isSatisfied) { throw `You must upgrade your Node.js installation to use kchatbotbak. Please visit ${chalk.green( 'https://nodejs.org/en/download/', )}`; } } export function getBotfrontVersion() { return JSON.parse( fs.readFileSync(path.join(__dirname, '../../kchatbotbak/package.json')), ).version; } export function isPrivate() { return JSON.parse(fs.readFileSync(path.join(__dirname, '../../kchatbotbak/package.json'))).private; } export function getComposeFilePath(dir, fileName = DOCKER_COMPOSE_FILENAME) { return path.join(fixDir(dir), BF_CONFIG_DIR, fileName); } export function getGeneratedComposeFilePath(dir, fileName = DOCKER_COMPOSE_FILENAME) { return path.join(fixDir(dir), fileName); } export function getProjectInfoDirPath(dir) { return path.join(fixDir(dir), BF_CONFIG_DIR); } export function getProjectInfoFilePath(dir) { return path.join(fixDir(dir), BF_CONFIG_DIR, BF_CONFIG_FILE); } export function getProjectEnvFilePath(dir) { return path.join(fixDir(dir), ENV_FILENAME); } export function isProjectDir(dir) { return fs.existsSync( getComposeFilePath(fixDir(dir), DOCKER_COMPOSE_TEMPLATE_FILENAME), ); } export function getComposeFile(dir, fileName) { return yaml.safeLoad(fs.readFileSync(getComposeFilePath(dir, fileName), 'utf-8')); } export function getGeneratedComposeFile(dir, fileName) { return yaml.safeLoad( fs.readFileSync(getGeneratedComposeFilePath(dir, fileName), 'utf-8'), ); } export function getServices(dir) { const services = getGeneratedComposeFile(dir).services; return Object.keys(services) .filter((s) => !!services[s].image) .map((s) => services[s].image); } export async function getMissingImgs(dir) { const docker = new Docker({}); let availableImgs = await docker.command( 'images --format "{{.Repository}}:{{.Tag}}"', ); availableImgs = availableImgs.raw.split('\n'); return getServices(dir).filter((service) => !availableImgs.includes(service)); } export function getServiceNames(dir) { const services = getGeneratedComposeFile(dir).services; return Object.keys(services); } export function getDefaultServiceNames(dir) { const services = getComposeFile(dir, DOCKER_COMPOSE_TEMPLATE_FILENAME).services; return Object.keys(services); } export function getService(serviceName, dir) { return getGeneratedComposeFile(dir).services[serviceName]; } export function getExternalPort(serviceName, dir) { return getService(serviceName, dir).ports[0].split(':')[0]; } export function getContainerAndImageNames(dir, services) { let svcs = services || getComposeFile(dir).services; return { containers: Object.keys(svcs).map((s) => services[s].container_name), images: Object.keys(svcs).map((s) => services[s].image), }; } export function getServiceUrl(serviceName) { return `http://localhost:${getExternalPort(serviceName)}`; } export function getComposeWorkingDir(workingDirectory) { return workingDirectory ? workingDirectory : './'; } export async function wait(millis) { return promisify(setTimeout)(millis); } export function capitalize(s) { if (typeof s !== 'string') return ''; return s.charAt(0).toUpperCase() + s.slice(1); } export async function shellAsync(command, options) { return promisify(shell.exec)(command, options); } export async function waitForService(serviceName) { const serviceUrl = getServiceUrl(serviceName); return new Promise((resolve, reject) => { const interval = setInterval(async () => { setTimeout(function () { reject(); }, 90000); let response; try { response = await axios.get(serviceUrl); } catch (e) { response = e.response; } if (response && [200, 404].includes(response.status)) { clearInterval(interval); return resolve(); } }, 1000); }); } export function randomString(length = 12) { var result = ''; var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'; var charactersLength = characters.length; for (var i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; }