@cto.ai/ops
Version:
š» CTO.ai Ops - The CLI built for Teams š
348 lines (347 loc) ⢠17.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("path"));
const yaml = tslib_1.__importStar(require("yaml"));
const base_1 = tslib_1.__importStar(require("../base"));
const asyncPipe_1 = require("../utils/asyncPipe");
const CustomErrors_1 = require("../errors/CustomErrors");
const opConfig_1 = require("../constants/opConfig");
const utils_1 = require("../utils");
const utils_2 = require("../utils");
class Init extends base_1.default {
constructor() {
super(...arguments);
this.questions = [];
this.srcDir = path.resolve(__dirname, '../templates/');
this.destDir = path.resolve(process.cwd());
this.initPrompts = {
[utils_1.appendSuffix(opConfig_1.COMMAND, 'Name')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Name'),
message: `\n Provide a name for your new command ${this.ux.colors.reset.green('ā')}\n${this.ux.colors.reset(this.ux.colors.secondary('Names must be lowercase'))}\n\nš· ${this.ux.colors.white('Name:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateName,
transformer: input => this.ux.colors.cyan(input.toLocaleLowerCase()),
filter: input => input.toLowerCase(),
},
[utils_1.appendSuffix(opConfig_1.COMMAND, 'Description')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Description'),
message: `\nProvide a description ${this.ux.colors.reset.green('ā')} \nāļø ${this.ux.colors.white('Description:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateDescription,
},
[utils_1.appendSuffix(opConfig_1.COMMAND, 'Version')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Version'),
message: `\nProvide a version ${this.ux.colors.reset.green('ā')} \nāļø ${this.ux.colors.white('Version:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateVersion,
default: '0.1.0',
},
[utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Name')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Name'),
message: `\n Provide a name for your new workflow ${this.ux.colors.reset.green('ā')}\n${this.ux.colors.reset(this.ux.colors.secondary('Names must be lowercase'))}\n\nš· ${this.ux.colors.white('Name:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateName,
transformer: input => this.ux.colors.cyan(input.toLocaleLowerCase()),
filter: input => input.toLowerCase(),
},
[utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Description')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Description'),
message: `\nProvide a description ${this.ux.colors.reset.green('ā')}\n\nāļø ${this.ux.colors.white('Description:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateDescription,
},
[utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Version')]: {
type: 'input',
name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Version'),
message: `\nProvide a version ${this.ux.colors.reset.green('ā')}\n\nāļø ${this.ux.colors.white('Version:')}`,
afterMessage: this.ux.colors.reset.green('ā'),
afterMessageAppend: this.ux.colors.reset(' added!'),
validate: this._validateVersion,
default: '0.1.0',
},
};
this.determineTemplate = async (prompts) => {
const { templates } = await this.ux.prompt({
type: 'checkbox',
name: 'templates',
message: `What type of op would you like to create ${this.ux.colors.reset.green('ā')}`,
choices: [
{
name: `${utils_1.titleCase(opConfig_1.COMMAND)} - A template for building commands which can be distributed via The Ops Platform.`,
value: opConfig_1.COMMAND,
},
{
name: `${utils_1.titleCase(opConfig_1.WORKFLOW)} - A template for combining many commands into a workflow which can be distributed via The Ops Platform.`,
value: opConfig_1.WORKFLOW,
},
],
afterMessage: `${this.ux.colors.reset.green('ā')}`,
validate: input => input.length != 0,
});
return { prompts, templates };
};
this.determineQuestions = ({ prompts, templates, }) => {
// Filters initPrompts based on the templates selected in determineTemplate
const removeIfNotSelectedTemplate = ([key, _val]) => {
return key.includes(templates[0]) || key.includes(templates[1]);
};
const questions = Object.entries(prompts)
.filter(removeIfNotSelectedTemplate)
.map(([_key, question]) => question);
return { questions, templates };
};
this.askQuestions = async ({ questions, templates, }) => {
const answers = await this.ux.prompt(questions);
return { answers, templates };
};
this.determineInitPaths = ({ answers, templates, }) => {
const initParams = Object.assign(Object.assign({}, answers), { templates });
const { name } = this.getNameAndDescription(initParams);
const sharedDir = `${this.srcDir}/shared`;
const destDir = `${this.destDir}/${name}`;
const initPaths = { sharedDir, destDir };
return { initPaths, initParams };
};
this.copyTemplateFiles = async ({ initPaths, initParams, }) => {
try {
const { templates } = initParams;
const { destDir, sharedDir } = initPaths;
await fs.ensureDir(destDir);
// copies op files if selected
if (templates.includes(opConfig_1.COMMAND)) {
await fs.copy(`${this.srcDir}/${opConfig_1.COMMAND}`, destDir);
}
// copies shared files
await fs.copy(sharedDir, destDir);
return { initPaths, initParams };
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.CopyTemplateFilesError(err);
}
};
this.customizePackageJson = async ({ initPaths, initParams, }) => {
try {
const { destDir, sharedDir } = initPaths;
const { name, description } = this.getNameAndDescription(initParams);
const packageObj = JSON.parse(fs.readFileSync(`${sharedDir}/package.json`, 'utf8'));
packageObj.name = name;
packageObj.description = description;
const newPackageString = JSON.stringify(packageObj, null, 2);
fs.writeFileSync(`${destDir}/package.json`, newPackageString);
return { initPaths, initParams };
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.CouldNotInitializeOp(err);
}
};
this.customizeYaml = async ({ initPaths, initParams, }) => {
try {
const { destDir } = initPaths;
// Parse YAML as document so we can work with comments
const opsYamlDoc = yaml.parseDocument(fs.readFileSync(`${destDir}/ops.yml`, 'utf8'));
await this.customizeOpsYaml(initParams, opsYamlDoc);
await this.customizeWorkflowYaml(initParams, opsYamlDoc);
// Process each root level section of the YAML file & add comments
Object.keys(opConfig_1.HELP_COMMENTS).forEach(rootKey => {
this.addHelpCommentsFor(rootKey, opsYamlDoc);
});
// Get the YAML file as string
const newOpsString = opsYamlDoc.toString();
fs.writeFileSync(`${destDir}/ops.yml`, newOpsString);
return { initPaths, initParams };
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.CouldNotInitializeOp(err);
}
};
// The `yaml` library has a pretty bad API for handling comments
// More: https://eemeli.org/yaml/#comments'
// TODO: Review type checking for yamlDoc (yaml.ast.Document) & remove tsignores
this.addHelpCommentsFor = (key, yamlDoc) => {
const docContents = yamlDoc.contents;
const docContentsItems = docContents.items;
const configItem = docContentsItems.find(item => {
if (!item || !item.key)
return;
const itemKey = item.key;
return itemKey.value === key;
});
// Simple config fields (`version`)
if (configItem &&
configItem.value &&
configItem.value.type === opConfig_1.YAML_TYPE_STRING &&
opConfig_1.HELP_COMMENTS[key]) {
configItem.comment = ` ${opConfig_1.HELP_COMMENTS[key]}`;
}
// Config fields with nested values (`ops`, `workflows`)
if (configItem &&
configItem.value &&
configItem.value.type === opConfig_1.YAML_TYPE_SEQUENCE) {
// @ts-ignore
yamlDoc.getIn([key, 0]).items.map(configItem => {
const comment = opConfig_1.HELP_COMMENTS[key][configItem.key];
if (comment)
configItem.comment = ` ${opConfig_1.HELP_COMMENTS[key][configItem.key]}`;
});
}
};
this.customizeOpsYaml = async (initParams, yamlDoc) => {
const { templates, commandName, commandDescription, commandVersion, } = initParams;
if (!templates.includes(opConfig_1.COMMAND)) {
// @ts-ignore
yamlDoc.delete('commands');
return;
}
yamlDoc
// @ts-ignore
.getIn(['commands', 0])
.set('name', `${commandName}:${commandVersion}`);
// @ts-ignore
yamlDoc.getIn(['commands', 0]).set('description', commandDescription);
};
this.customizeWorkflowYaml = async (initParams, yamlDoc) => {
const { templates, workflowName, workflowDescription, workflowVersion, } = initParams;
if (!templates.includes(opConfig_1.WORKFLOW)) {
// @ts-ignore
yamlDoc.delete('workflows');
return;
}
yamlDoc
// @ts-ignore
.getIn(['workflows', 0])
.set('name', `${workflowName}:${workflowVersion}`);
// @ts-ignore
yamlDoc.getIn(['workflows', 0]).set('description', workflowDescription);
};
this.logMessages = async ({ initPaths, initParams, }) => {
const { destDir } = initPaths;
const { templates } = initParams;
const { name } = this.getNameAndDescription(initParams);
this.logSuccessMessage(templates);
fs.readdirSync(`${destDir}`).forEach((file) => {
let callout = '';
if (file.indexOf('index.js') > -1) {
callout = `${this.ux.colors.green('ā')} ${this.ux.colors.white('Start developing here!')}`;
}
let msg = this.ux.colors.italic(`${path.relative(this.destDir, process.cwd())}/${name}/${file} ${callout}`);
this.log(`š .${msg}`);
});
if (templates.includes(opConfig_1.COMMAND)) {
this.logCommandMessage(initParams);
}
if (templates.includes(opConfig_1.WORKFLOW)) {
this.logWorkflowMessage(initParams);
}
return { initPaths, initParams };
};
this.logCommandMessage = (initParams) => {
const { commandName } = initParams;
this.log(`\nš To test your ${opConfig_1.COMMAND} run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`ops run ${commandName}`)}`);
};
this.logWorkflowMessage = (initParams) => {
const { workflowName } = initParams;
const { name } = this.getNameAndDescription(initParams);
this.log(`\nš To test your ${opConfig_1.WORKFLOW} run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`cd ${name} && npm install && ops run .`)}`);
};
this.logSuccessMessage = (templates) => {
const successMessageBoth = `\nš Success! Your ${opConfig_1.COMMAND} and ${opConfig_1.WORKFLOW} template Ops are ready to start coding... \n`;
const getSuccessMessage = (opType) => `\nš Success! Your ${opType} template Op is ready to start coding... \n`;
if (templates.includes(opConfig_1.COMMAND) && templates.includes(opConfig_1.WORKFLOW)) {
return this.log(successMessageBoth);
}
const opType = templates.includes(opConfig_1.COMMAND) ? opConfig_1.COMMAND : opConfig_1.WORKFLOW;
return this.log(getSuccessMessage(opType));
};
this.sendAnalytics = async ({ initPaths, initParams, }) => {
try {
const { destDir } = initPaths;
const { templates } = initParams;
const { name, description } = this.getNameAndDescription(initParams);
this.services.analytics.track({
userId: this.user.email,
teamId: this.team.id,
cliEvent: 'Ops CLI Init',
event: 'Ops CLI Init',
properties: {
name,
team: this.team.name,
namespace: `@${this.team.name}/${name}`,
runtime: 'CLI',
email: this.user.email,
username: this.user.username,
path: destDir,
description,
templates,
},
}, this.accessToken);
return {
initPaths,
initParams,
};
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.AnalyticsError(err);
}
};
this.getNameAndDescription = (initParams) => {
return {
name: initParams.commandName || initParams.workflowName,
description: initParams.commandDescription || initParams.workflowDescription,
};
};
}
_validateName(input) {
if (input === '')
return 'You need name your op before you can continue';
if (!input.match('^[a-z0-9_-]*$')) {
return 'Sorry, please name the Op using only numbers, letters, -, or _';
}
return true;
}
_validateDescription(input) {
if (input === '')
return 'You need to provide a description of your op before continuing';
return true;
}
_validateVersion(input) {
if (input === '')
return 'You need to provide a version of your op before continuing';
if (!input.match(utils_2.validVersionChars)) {
return `Sorry, version can only contain letters, digits, underscores, periods and dashes\nand must start and end with a letter or a digit`;
}
return true;
}
async run() {
this.parse(Init);
try {
await this.isLoggedIn();
const initPipeline = asyncPipe_1.asyncPipe(this.determineTemplate, this.determineQuestions, this.askQuestions, this.determineInitPaths, this.copyTemplateFiles, this.customizePackageJson, this.customizeYaml, this.sendAnalytics, this.logMessages);
await initPipeline(this.initPrompts);
}
catch (err) {
this.debug('%O', err);
this.config.runHook('error', { err, accessToken: this.accessToken });
}
}
}
exports.default = Init;
Init.description = 'Easily create a new Op.';
Init.flags = {
help: base_1.flags.help({ char: 'h' }),
};