UNPKG

@cto.ai/ops

Version:

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

348 lines (347 loc) • 17.5 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 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' }), };