@cto.ai/ops
Version:
💻 CTO.ai - The CLI built for Teams 🚀
383 lines (382 loc) • 16.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const cli_sdk_1 = require("@cto.ai/cli-sdk");
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("path"));
const opConfig_1 = require("../constants/opConfig");
const OpsYml_1 = require("../types/OpsYml");
const arrayUtils_1 = require("../utils/arrayUtils");
const base_1 = tslib_1.__importStar(require("./../base"));
const yaml = tslib_1.__importStar(require("yaml"));
const CustomErrors_1 = require("./../errors/CustomErrors");
const utils_1 = require("./../utils");
const templateUtils_1 = require("./../utils/templateUtils");
const filterOnlyDirectories = (dirname, entries) => {
return entries.filter(entry => {
try {
return fs.statSync(path.join(dirname, entry)).isDirectory();
}
catch (e) {
return false;
}
});
};
const directoriesIn = async (dirname) => {
return filterOnlyDirectories(dirname, await fs.readdir(dirname));
};
const checkForSpecialFiles = (dirname) => {
return {
pjson: fs.existsSync(path.join(dirname, 'package.json')),
};
};
const NAME_REGEX = /^[a-z0-9_-]*$/;
const validateName = (input) => {
if (input === '')
return 'You need name your Application before you can continue';
if (!input.match(NAME_REGEX)) {
return 'Sorry, please name the Application using only numbers, lowercase letters, -, or _';
}
return true;
};
const validateDescription = (input) => {
if (input === '')
return 'You need to provide a description of your application before continuing';
return true;
};
const validateVersion = (input) => {
if (input === '')
return 'You need to provide a version of your application before continuing';
if (!input.match(utils_1.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;
};
class Init extends base_1.default {
constructor() {
super(...arguments);
this.srcDir = path.resolve(__dirname, '../templates/');
this.destDir = path.resolve(process.cwd());
this.readTemplates = async () => {
const kinds = await directoriesIn(this.srcDir);
const templates = {};
for (const kind of kinds) {
const langNames = await directoriesIn(path.join(this.srcDir, kind));
if (langNames.length === 0) {
continue;
}
const langs = {};
for (const lang of langNames) {
const templatePath = path.join(this.srcDir, kind, lang);
langs[lang] = {
kind,
name: lang,
// We push the JS option to the top of the list
priority: lang === 'JavaScript',
path: templatePath,
specialFiles: checkForSpecialFiles(templatePath),
};
}
templates[kind] = langs;
}
return templates;
};
this.selectKind = async (kinds, flagKind) => {
if (flagKind) {
if (kinds.includes(flagKind)) {
return flagKind;
}
this.log(`No templates found to match ${flagKind}, please select a different kind`);
}
try {
return await this.pickFromList(kinds, 'What kind of Workflow would you like?');
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.EnumeratingLangsError(err);
}
};
this.selectTemplateName = async (templates, flagTemplate) => {
if (flagTemplate) {
if (templates.includes(flagTemplate)) {
return flagTemplate;
}
this.log(`No templates found named ${flagTemplate}, please select a different template`);
}
try {
return await this.pickFromList(templates, 'Which template would you like?');
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.EnumeratingLangsError(err);
}
};
/**
* Returns the list of templates available for the user based provided flags
* and selected languague.
*
* @remarks
* This method will ignore any template directory prefixed with `_`
*
* @param templates - Available templates for the given Ops type
* @param flags - The cli flags provided by the user
* @returns Promise<TemplateDefinition> The selected template
*
*/
this.selectTemplate = async (templates, flags) => {
const langs = templates[await this.selectKind(Object.keys(templates), flags.kind)];
let langNames = Object.keys(langs).sort((a, b) => {
// NOTE: We're assuming a single `priority` template here
if (a === b) {
return 0;
}
else if (langs[a].priority) {
return -1;
}
else if (langs[b].priority) {
return 1;
}
else {
return a > b ? 1 : -1;
}
});
langNames = langNames.filter(l => !l.startsWith('_'));
return langs[await this.selectTemplateName(langNames, flags.template)];
};
this.promptForName = async (name, kind) => {
if (name) {
const validation = validateName(name);
if (validation == true) {
return name;
}
this.log(validation);
}
const promptResult = await this.ux.prompt({
type: 'input',
name: 'name',
message: `\n Provide a name for your new ${kind} ${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: validateName,
transformer: input => this.ux.colors.cyan(input.toLocaleLowerCase()),
filter: input => input.toLowerCase(),
});
return promptResult.name;
};
this.promptForMetadata = async (template, nameParam) => {
const name = await this.promptForName(nameParam, template.kind);
const { description } = await this.ux.prompt({
type: 'input',
name: '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: validateDescription,
});
const { version } = await this.ux.prompt({
type: 'input',
name: '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: validateVersion,
default: '0.1.0',
});
return { name, description, version };
};
this.customizeSpecialFiles = async (template, metadata, targetPath) => {
if (template.kind === 'pipeline') {
// update the pipeline step name to avoid publish collisions
const opsYMLPath = path.join(targetPath, 'ops.yml');
try {
const opsYMLObj = yaml.parse(await fs.readFile(opsYMLPath, 'utf8'));
const pipeline = opsYMLObj.pipelines[0];
pipeline.jobs.forEach(job => {
job.name = `${metadata.name}-${job.name}`;
});
await fs.writeFile(opsYMLPath, yaml.stringify(opsYMLObj));
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.CouldNotInitializeOp(err);
}
}
if (!template.specialFiles.pjson) {
return;
}
const pjsonPath = path.join(targetPath, 'package.json');
try {
const pjsonObj = JSON.parse(await fs.readFile(pjsonPath, 'utf8'));
pjsonObj.name = metadata.name;
pjsonObj.description = metadata.description;
await fs.writeFile(pjsonPath, JSON.stringify(pjsonObj, null, 2));
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.CouldNotInitializeOp(err);
}
};
this.sendAnalytics = (config, kind, metadata, targetPath) => {
try {
this.services.analytics.track('Ops CLI Init', {
name: metadata.name,
namespace: `@${config.team.name}/${metadata.name}`,
runtime: 'CLI',
username: config.user.username,
path: targetPath,
description: metadata.description,
templates: [kind],
}, config);
}
catch (err) {
this.debug('%O', err);
throw new CustomErrors_1.AnalyticsError(err);
}
};
this.selectOpToBuild = async (ops) => {
if (ops.length === 1) {
return ops;
}
const { opsToBuild } = await cli_sdk_1.ux.prompt({
type: 'checkbox',
name: 'opsToBuild',
message: `\n Which workflows would you like to build ${cli_sdk_1.ux.colors.reset.green('→')}`,
choices: ops.map(op => {
return {
value: op,
name: `${op.name} - ${op.description}`,
};
}),
validate: input => input.length > 0,
});
return opsToBuild;
};
this.convertOpsToCommands = async (opsToBuild, opPath) => {
let convertedOps = [];
convertedOps = opsToBuild.map(async (x) => {
if (x.type === opConfig_1.PIPELINE_TYPE) {
return this.services.opService.convertPipelinesToOps([x], this.state.config, opPath);
}
else if (x.type === opConfig_1.SERVICE_TYPE) {
return (0, OpsYml_1.convertServicesToOps)([x]);
}
else {
return x;
}
});
return (0, arrayUtils_1.flatten)(convertedOps);
};
}
async run() {
const { flags, args: { name }, } = this.parse(Init);
if (flags.jobs) {
let opPath = process.cwd();
if (name !== '.' && name.length > 0) {
opPath = path.join(opPath, name);
}
const inputManifest = await this.services.opService.getOpsFromFileSystem(opPath);
const ops = [
...inputManifest.ops,
...inputManifest.pipelines,
...inputManifest.services,
];
const opsToBuild = await this.selectOpToBuild(ops);
if (!opsToBuild.length) {
throw new CustomErrors_1.NoOpsToBuildFound();
}
await this.convertOpsToCommands(opsToBuild, opPath);
this.log(`\n🎉 Success! We've created a basic template for you: \n`);
this.log(`\n🚀 To build & run your Workflow run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`ops run -b ${name}`)}`);
return;
}
const config = await this.isLoggedIn();
const exec = require('child_process').exec;
const gurl = require('parse-github-url');
// check if template is provided
let inputs = gurl(name);
if (inputs && inputs.owner && inputs.name) {
this.log(`\n${this.ux.colors.green('→ Downloading')} ${name}...\n`);
var download = {
branch: inputs.branch || 'main',
owner: inputs.owner || 'cto-ai',
name: inputs.name || 'examples',
};
let org = download.owner;
let repo = download.name;
let branch = download.branch;
let path = name.split(branch)[1] || '';
let dirs = path.split('/').filter(v => v != '');
let strip = dirs.length;
let cmd = `mkdir ${repo} && ` +
`curl -sL https://github.com/${org}/${repo}/tarball/${branch} ` +
`| tar xvz -C ./${repo} --strip-components=1`;
await new Promise((resolve, reject) => {
let clone = exec(`${cmd}`);
clone.stdout.pipe(process.stdout);
clone.stderr.pipe(process.stderr);
clone.on('exit', status => {
status === 0
? resolve(`${this.ux.colors.green('✓ ' + name)} has been downloaded successfully.`)
: reject(`${this.ux.colors.red('❗Could not download the template. Please try again.')}`);
});
})
.then(msg => console.log(`\n${msg}`))
.catch(err => console.log(`\n${err}`));
}
else {
try {
const templates = await this.readTemplates();
const template = await this.selectTemplate(templates, flags);
const metadata = await this.promptForMetadata(template, name);
const targetPath = path.join(this.destDir, metadata.name);
await (0, templateUtils_1.copyTemplate)(template.path, targetPath, {
filter: filePath => !filePath.includes('.npmignore'),
});
await this.customizeSpecialFiles(template, metadata, targetPath);
await (0, templateUtils_1.customizeManifest)(template.kind, metadata, targetPath);
this.sendAnalytics(config, template.kind, metadata, targetPath);
this.log(`\n🎉 Success! We've created a basic template for you: \n`);
const fileList = await fs.readdir(targetPath);
const relativePath = path.relative(process.cwd(), targetPath);
for (const file of fileList) {
let message = path.join(relativePath, file);
if (file.includes('main') || file.includes('index')) {
message += ` ${this.ux.colors.green('←')} ${this.ux.colors.white('Start developing here!')}`;
}
this.log(`📁 ./${this.ux.colors.italic(message)}`);
}
this.log(`\n🚀 To build & run your Workflow run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`ops run -b ${metadata.name}`)}`);
}
catch (err) {
this.debug('%O', err);
this.config.runHook('error', {
err,
accessToken: config.tokens.accessToken,
});
}
}
}
}
exports.default = Init;
Init.description = 'Create a new Workflow';
Init.flags = {
help: base_1.flags.help({ char: 'h' }),
kind: base_1.flags.string({
char: 'k',
description: 'the kind of Application to create (command, pipeline, etc.)',
}),
jobs: base_1.flags.boolean({
char: 'j',
description: 'generate local template files for pipeline jobs',
}),
template: base_1.flags.string({
char: 't',
description: 'the name of the template to use',
}),
};
Init.args = [
{
name: 'name',
description: 'provide a name or pass a github url to a template',
},
];