jessyfront
Version:
Botfront is an open source chatbot platform based on Rasa.
212 lines (197 loc) • 10 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,
getServices,
getMissingImgs,
waitForService,
getServiceUrl,
getComposeWorkingDir,
wait,
shellAsync,
getServiceNames,
capitalize,
generateDockerCompose,
startSpinner,
stopSpinner,
failSpinner,
succeedSpinner,
consoleError,
setSpinnerText,
setSpinnerInfo,
updateEnvFile,
shouldUpdateProject,
displayUpdateMessage,
} from '../utils';
export async function dockerComposeUp({ verbose = false }, workingDir, spinner = ora()) {
await displayUpdateMessage();
const projectAbsPath = fixDir(workingDir);
shell.cd(projectAbsPath);
if (!isProjectDir()) {
const noProjectMessage = `${chalk.yellow.bold('No project found.')} ${chalk.cyan.bold('botfront 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 Botfront. Would you like to upgrade it?`,
});
if (upgrade) {
await copyTemplateFilesToProjectDir(projectAbsPath, {}, true);
}
}
updateEnvFile(process.cwd());
generateDockerCompose();
startSpinner(spinner, 'Starting Botfront...')
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 Botfront...')
await shellAsync(command, { silent: !verbose });
await waitForService('botfront');
stopSpinner()
const serviceUrl = getServiceUrl('botfront');
console.log(`\n\n 🎉 🎈 Botfront is ${chalk.green.bold('UP')}! 🎉 🎈\n`);
const message = `Useful commands:\n\n` +
`\u2022 Run ${chalk.cyan.bold('botfront logs')} to see logs and debug \n` +
`\u2022 Run ${chalk.cyan.bold('botfront <stop|start|restart>')} to <stop|start|restart> a service \n` +
`\u2022 Run ${chalk.cyan.bold('botfront down')} to stop Botfront\n` +
`\u2022 Run ${chalk.cyan.bold('botfront --help')} to get help with the CLI\n`;
`\u2022 Run ${chalk.cyan.bold('botfront docs')} to browse the online documentation\n`;
console.log(boxen(message) + '\n');
setSpinnerText(spinner, `Opening Botfront (${chalk.green.bold(serviceUrl)}) in your browser...`);
await wait(2000);
if (spinner) await open(serviceUrl);
setSpinnerInfo(spinner, `Visit ${chalk.green(serviceUrl)}`);
console.log('\n');
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('botfront logs')}.`);
} else {
stopSpinner(spinner);
failSpinner(spinner, 'Couldn\'t start Botfront. Retrying in verbose mode...');
return dockerComposeUp({ verbose: true }, workingDir, null, spinner);
}
}
}
export async function dockerComposeDown({ verbose }, workingDir) {
if (workingDir) shell.cd(workingDir)
if (!isProjectDir()) {
const noProjectMessage = `${chalk.yellow.bold('No project found in this directory.')}\n\nIf you don\'t know where your project is running from,\n${chalk.cyan.bold('botfront killall')} will find and shut down any Botfront\nproject on your machine.`;
return console.log(boxen(noProjectMessage));
}
const spinner = ora('Stopping Botfront...')
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(`botfront ${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);
if (!service || !allowedServices.includes(service)) {
const services = 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,
});
service = serv;
}
const spinner = ora(service ? `${capitalize(action)} service ${service}...`: `${capitalize(action)} all services...`)
spinner.start();
let command = `docker-compose -f ${getComposeFilePath()} --project-directory ${getComposeWorkingDir(workingDir)} ${name} ${allowedServices.includes(service) ? service : ''}`;
await shellAsync(command, { silent: !verbose })
spinner.succeed(`Done. ${message}`);
}
export function dockerComposeFollow(commander, 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 -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(/(botfront-\w+)/g);
const networkCommand = await docker.command('network ls --format={{.Name}}');
const networks = networkCommand.raw.match(/([^\s]+_botfront-network)/g);
const volumeCommand = await docker.command('volume ls --format={{.Name}}');
const volumes = volumeCommand.raw.match(/([^\s]+_botfront-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(" ")}`);
} catch(e) {
failSpinner(spinner, `Could not stop running project. Run ${chalk.cyan.bold('docker ps | grep -w botfront-')} 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`);
} finally {
stopSpinner();
}
}
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}...`);
});
}