@cto.ai/ops-rc
Version:
š» CTO.ai Ops - The CLI built for Teams š
251 lines (250 loc) ⢠10.7 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 base_1 = tslib_1.__importStar(require("./../base"));
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 op before you can continue';
if (!input.match(NAME_REGEX)) {
return 'Sorry, please name the Op using only numbers, lowercase letters, -, or _';
}
return true;
};
const validateDescription = (input) => {
if (input === '')
return 'You need to provide a description of your op before continuing';
return true;
};
const validateVersion = (input) => {
if (input === '')
return 'You need to provide a version of your op 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 for Op kind ${flagKind}, please select a different kind`);
}
try {
return await this.pickFromList(kinds, 'What kind of Op 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);
}
};
this.selectTemplate = async (templates, flags) => {
const langs = templates[await this.selectKind(Object.keys(templates), flags.kind)];
const 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;
}
});
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) => {
// we only have package.json for now
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);
}
};
}
async run() {
const { flags, args: { name }, } = this.parse(Init);
const config = await this.isLoggedIn();
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 templateUtils_1.copyTemplate(template.path, targetPath);
await this.customizeSpecialFiles(template, metadata, targetPath);
await templateUtils_1.customizeManifest(template.kind, metadata, targetPath);
this.sendAnalytics(config, template.kind, metadata, targetPath);
this.log(`\nš Success! Your new Op is ready to start coding... \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 try out your Op run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`ops run ${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 op';
Init.flags = {
help: base_1.flags.help({ char: 'h' }),
kind: base_1.flags.string({
char: 'k',
description: 'the kind of op to create (command, pipeline, etc.)',
}),
template: base_1.flags.string({
char: 't',
description: 'the name of the template to use',
}),
};
Init.args = [{ name: 'name', description: 'the name of the op to create' }];