UNPKG

@cto.ai/ops-rc

Version:

šŸ’» CTO.ai Ops - The CLI built for Teams šŸš€

251 lines (250 loc) • 10.7 kB
"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' }];