brainwise-demo-cli
Version:
brainwise-Newcli is an open source chatbot platform based on Rasa.
256 lines (237 loc) • 11.6 kB
JavaScript
import shell from 'shelljs';
import open from 'open';
import { Docker } from 'docker-cli-js';
import ora from 'ora';
import chalk from 'chalk';
import inquirer from 'inquirer';
import boxen from 'boxen';
import { pullDockerImages, copyTemplateFilesToProjectDir } from './init';
import path from 'path';
import { watch } from 'chokidar';
import {
fixDir,
isProjectDir,
getComposeFilePath,
getMissingImgs,
waitForService,
getServiceUrl,
getComposeWorkingDir,
wait,
shellAsync,
getServiceNames,
capitalize,
generateDockerCompose,
startSpinner,
stopSpinner,
failSpinner,
succeedSpinner,
consoleError,
setSpinnerText,
setSpinnerInfo,
updateEnvFile,
shouldUpdateProject,
displayUpdateMessage,
getDefaultServiceNames,
} from '../utils';
async function postUpLaunch(spinner) {
const serviceUrl = getServiceUrl('brainwise');
setSpinnerText(spinner, `Opening Brainwise-cli (${chalk.green.bold(serviceUrl)}) in your browser...`);
await wait(2000);
await open(serviceUrl);
setSpinnerInfo(spinner, `Visit ${chalk.green(serviceUrl)}`);
console.log('\n');
}
export async function dockerComposeUp({ verbose = false, exclude = [], ci = false }, workingDir, spinner) {
spinner = spinner
? spinner
: ci
? null
: ora();
await displayUpdateMessage();
const projectAbsPath = fixDir(workingDir);
shell.cd(projectAbsPath);
if (!isProjectDir()) {
const noProjectMessage = `${chalk.yellow.bold('No project found.')} ${chalk.cyan.bold('brainwise-demo-cli up')} must be executed from your project\'s directory`;
return console.log(noProjectMessage);
}
if (shouldUpdateProject()) {
const { upgrade } = await inquirer.prompt({
type: 'confirm',
name: 'upgrade',
message: 'Your project was created with an older version of Brainwise-cli. Would you like to upgrade it?',
});
if (upgrade) {
await copyTemplateFilesToProjectDir(projectAbsPath, {}, true);
}
}
updateEnvFile(process.cwd());
await generateDockerCompose(exclude);
startSpinner(spinner, 'Starting Brainwise-cli...');
const missingImgs = await getMissingImgs();
await pullDockerImages(missingImgs, spinner, 'Downloading Docker images...');
await stopRunningProjects('Shutting down running project first...', null, null, spinner);
let command = `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} up -d`;
try {
startSpinner(spinner, 'Starting Brainwise-cli...')
await shellAsync(command, { silent: !verbose });
if (ci) process.exit(0); // exit now if ci
if (!exclude.includes('brainwise')) await waitForService('brainwise');
stopSpinner();
console.log(`\n\n 🎉 🎈 Brainwise-cli is ${chalk.green.bold('UP')}! 🎉 🎈\n`);
const message = 'Useful commands:\n\n' + (
`\u2022 Run ${chalk.cyan.bold('brainwise-demo-cli logs')} to see logs and debug \n` +
`\u2022 Run ${chalk.cyan.bold('brainwise-demo-cli <stop|start|restart>')} to <stop|start|restart> a service \n` +
`\u2022 Run ${chalk.cyan.bold('brainwise-demo-cli down')} to stop Brainwise-cli\n` +
`\u2022 Run ${chalk.cyan.bold('brainwise-demo-cli --help')} to get help with the CLI\n` +
`\u2022 Run ${chalk.cyan.bold('brainwise-demo-cli docs')} to browse the online documentation\n`
);
console.log(boxen(message) + '\n');
if (!exclude.includes('brainwise')) await postUpLaunch(spinner); // browser stuff is brainwise is not excluded
stopSpinner();
process.exit(0);
} catch (e) {
if (verbose) {
failSpinner(spinner, `${chalk.red.bold('ERROR:')} Something went wrong. Check the logs above for more information ☝️, or try inspecting the logs with ${chalk.red.cyan('brainwise-cli logs')}.`);
} else {
stopSpinner(spinner);
failSpinner(spinner, 'Couldn\'t start brainwise-demo-cli. Retrying in verbose mode...', { exit: false });
return dockerComposeUp({ verbose: true, exclude: exclude }, workingDir, null, spinner);
}
}
}
export async function dockerComposeDown({ verbose }, workingDir) {
if (workingDir) shell.cd(workingDir)
if (!isProjectDir()) {
const noProject = chalk.yellow.bold('No project found in this directory.');
const killall = chalk.cyan.bold('brainwise-demo-cli killall');
const noProjectMessage = (
`${noProject}\n\nIf you don't know where your project is running from,\n` +
`${killall} will find and shut down any brainwise chatbot\nproject on your machine.`
);
return console.log(boxen(noProjectMessage));
}
const spinner = ora('Stopping brainwise-demo-cli...')
spinner.start();
let command = `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} down`;
await shellAsync(command, { silent: !verbose })
spinner.succeed('All services are stopped. Come back soon... 🤗');
}
export async function dockerComposeStop(service, { verbose }, workingDir) {
await dockerComposeCommand(service, {name: 'stop', action: 'stopping'}, verbose, workingDir )
.catch(consoleError)
}
export async function dockerComposeStart(service, { verbose }, workingDir) {
await dockerComposeCommand(service, {name: 'start', action: 'starting'}, verbose, workingDir, 'It might take a few seconds before services are available...' )
.catch(consoleError)
}
export async function dockerComposeRestart(service, { verbose }, workingDir) {
await dockerComposeCommand(service, {name: 'restart', action: 'restarting'}, verbose, workingDir, 'It might take a few seconds before services are available...' )
.catch(consoleError)
}
export async function dockerComposeCommand(service, {name, action}, verbose, workingDir, message = '') {
if (workingDir) shell.cd(workingDir)
if (!isProjectDir()) {
const noProjectMessage = `${chalk.yellow.bold('No project found in this directory.')}\n` + (
`${chalk.cyan.bold(`brainwise-demo-cli ${name}`)} must be executed in your project's directory.\n` +
`${chalk.green.bold('TIP: ')}if you just created your project, you probably just have to` +
` do ${chalk.cyan.bold('cd <your-project-folder>')} and then retry.`
);
return console.log(boxen(noProjectMessage));
}
const allowedServices = getServiceNames(workingDir);
let services = [service];
let regeneratedDockerCompose = false;
if (!service || !allowedServices.includes(service)) {
const defaultServices = getDefaultServiceNames(workingDir);
if (name === 'start' && defaultServices.includes(service)) {
// service had been excluded, regenerate docker compose file
const exclude = defaultServices.filter(s => ![...allowedServices, service].includes(s))
regeneratedDockerCompose = await generateDockerCompose(exclude);
} else {
const choices = allowedServices.concat(`${capitalize(name)} all services`);
const { serv } = await inquirer.prompt({
type: 'list',
name: 'serv',
message: `Which service do you want to ${name}?`,
choices,
});
services = serv.endsWith('all services')
? allowedServices
: [serv];
}
}
const spinner = ora(`${capitalize(action)} ${services.join(', ')}...`)
spinner.start();
const command = regeneratedDockerCompose // if docker-compose file has been regenerated, run 'up -d' instead of 'start', to create container
? `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} up -d`
: `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} ${name} ${services.join(' ')}`;
await shellAsync(command, { silent: !verbose })
spinner.succeed(`Done. ${message}`);
}
export function dockerComposeFollow({ ci = false }, workingDir) {
if (workingDir) shell.cd(workingDir)
if (!isProjectDir()) {
const noProjectMessage = `${chalk.yellow.bold('No project found in this directory.')}\nThis command must be executed in your project's root directory.\n`+
`${chalk.green.bold('TIP: ')}if you just created your project, you probably just have to do ${chalk.cyan.bold('cd <your-project-folder>')} and then retry`;
return console.log(boxen(noProjectMessage));
}
let command = `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} logs${!ci ? ' -f' : ''}`;
shell.exec(command)
}
export async function getRunningDockerResources() {
const docker = new Docker({});
const command = 'container ps --format={{.Names}}'
const containersCommand = await docker.command(command);
const containers = containersCommand.raw.match(/(brainwise-\w+)/g);
const networkCommand = await docker.command('network ls --format={{.Name}}');
const networks = networkCommand.raw.match(/([^\s]+_brainwise-network)/g);
const volumeCommand = await docker.command('volume ls --format={{.Name}}');
const volumes = volumeCommand.raw.match(/([^\s]+_brainwise-db)/g);
return { containers, networks, volumes };
}
export async function stopRunningProjects(
runningMessage=null,
killedMessage = null,
allDeadMessage = null,
spinner,
) {
const docker = new Docker({});
try{
const { containers, networks, volumes } = await getRunningDockerResources();
if (containers && containers.length) {
startSpinner(spinner, runningMessage)
await docker.command(`stop ${containers.join(' ')}`);
await docker.command(`rm ${containers.join(' ')}`);
if (killedMessage) succeedSpinner(spinner, killedMessage);
} else {
if (allDeadMessage) succeedSpinner(spinner, allDeadMessage)
}
if (volumes && volumes.length) await docker.command(`volume rm ${volumes.join(' ')}`)
if (networks && networks.length) await docker.command(`network rm ${networks.join(' ')}`);
stopSpinner();
} catch(e) {
stopSpinner();
const failMessage = `Could not stop running project. Run ${chalk.cyan.bold('docker ps | grep -w Brainwise-cli-')} and ` + (
`then ${chalk.cyan.bold('docker stop <name>')} and ${chalk.cyan.bold('docker rm <name>')} for ` +
`each running container.\n${chalk.red.bold(e)}\n`
);
failSpinner(spinner, failMessage);
}
}
export async function watchFolder({ verbose }, workingDir) {
const spinner = ora();
const watchedPath = path.join(fixDir(), 'actions');
const service = 'actions';
startSpinner(spinner, `Watching for file system changes in ${watchedPath}...`);
watch(watchedPath, {
ignored: /(^|[\/\\])\../,
ignoreInitial: true,
interval: 1000,
})
.on('all', async (event, path) => {
stopSpinner(spinner);
console.log(`Detected ${event} on ${path}.`);
await dockerComposeRestart(service, { verbose }, workingDir);
startSpinner(spinner, `Watching for file system changes in ${watchedPath}...`);
});
}