UNPKG

@cto.ai/ops

Version:

💻 CTO.ai - The CLI built for Teams 🚀

383 lines (382 loc) 16.6 kB
"use strict"; 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', }, ];