UNPKG

@cto.ai/ops

Version:

💻 CTO.ai Ops - The CLI built for Teams 🚀

377 lines (376 loc) 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const fuzzy_1 = tslib_1.__importDefault(require("fuzzy")); 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 opConfig_1 = require("../constants/opConfig"); const env_1 = require("../constants/env"); const utils_1 = require("../utils"); const validate_1 = require("../utils/validate"); class Run extends base_1.default { constructor() { super(...arguments); this.opsAndWorkflows = []; this.customParse = (options, argv) => { const { args, flags } = require('@oclif/parser').parse(argv, Object.assign(Object.assign({}, options), { context: this })); if (!args.nameOrPath && !flags.help) { throw new CustomErrors_1.MissingRequiredArgument('ops run'); } if (!args.nameOrPath) this._help(); return { args, flags, opParams: argv.slice(1) }; }; this.checkPathOpsYmlExists = (nameOrPath) => { const pathToOpsYml = path.join(path.resolve(nameOrPath), opConfig_1.OP_FILE); return fs.existsSync(pathToOpsYml); }; this.parseYamlFile = async (relativePathToOpsYml) => { const opsYmlExists = this.checkPathOpsYmlExists(relativePathToOpsYml); if (!opsYmlExists) { return null; } const opsYml = await fs.readFile(path.join(path.resolve(relativePathToOpsYml), opConfig_1.OP_FILE), 'utf8'); const { ops = [], workflows = [], version = '1' } = (await utils_1.parseYaml(opsYml)); return { ops, workflows, version }; }; this.logResolvedLocalMessage = (inputs) => { const { parsedArgs: { args: { nameOrPath }, }, } = inputs; this.log(`❗️ ${this.ux.colors.callOutCyan(nameOrPath)} ${this.ux.colors.white('resolved to a local path and is running local Op.')} `); return inputs; }; /* get all the commands and workflows in an ops.yml that match the nameOrPath */ this.getOpsAndWorkflowsFromFileSystem = (relativePathToOpsYml) => async (inputs) => { const yamlContents = await this.parseYamlFile(relativePathToOpsYml); if (!yamlContents) { return Object.assign(Object.assign({}, inputs), { opsAndWorkflows: [] }); } const { ops, workflows } = yamlContents; return Object.assign(Object.assign({}, inputs), { opsAndWorkflows: [...ops, ...workflows] }); }; this.addMissingApiFieldsToLocalOps = async (inputs) => { const { opsAndWorkflows, config } = inputs; const updatedOpsAndWorkflows = opsAndWorkflows.map((opOrWorkflow) => { let newOpOrWorkflow = Object.assign({}, opOrWorkflow); newOpOrWorkflow.teamName = config.team.name; newOpOrWorkflow.type = 'steps' in newOpOrWorkflow ? opConfig_1.WORKFLOW_TYPE : opConfig_1.COMMAND_TYPE; return newOpOrWorkflow; }); return Object.assign(Object.assign({}, inputs), { opsAndWorkflows: updatedOpsAndWorkflows }); }; this.filterLocalOps = (inputs) => { const { opsAndWorkflows } = inputs; if (!opsAndWorkflows) { return Object.assign({}, inputs); } const { parsedArgs: { args: { nameOrPath }, }, } = inputs; const keepOnlyMatchingNames = ({ name }) => { return name.indexOf(nameOrPath) >= 0; }; return Object.assign(Object.assign({}, inputs), { opsAndWorkflows: opsAndWorkflows.filter(keepOnlyMatchingNames) }); }; this.formatOpOrWorkflowEmoji = (opOrWorkflow) => { if (!opOrWorkflow.isPublished) { return '🖥 '; } else if (opOrWorkflow.isPublic) { return '🌎 '; } else { return '🔑 '; } }; this.formatOpOrWorkflowName = (opOrWorkflow) => { const name = this.ux.colors.reset.white(opOrWorkflow.name); if ((!opOrWorkflow.isPublished && 'steps' in opOrWorkflow) || (opOrWorkflow.isPublished && opOrWorkflow.type === opConfig_1.WORKFLOW_TYPE)) { return `${this.ux.colors.reset(this.ux.colors.multiOrange('\u2022'))} ${this.formatOpOrWorkflowEmoji(opOrWorkflow)} ${name}`; } else { return `${this.ux.colors.reset(this.ux.colors.multiBlue('\u2022'))} ${this.formatOpOrWorkflowEmoji(opOrWorkflow)} ${name}`; } }; this.fuzzyFilterParams = () => { const list = this.opsAndWorkflows.map(opOrWorkflow => { const name = this.formatOpOrWorkflowName(opOrWorkflow); return { name: `${name} - ${opOrWorkflow.description}`, value: opOrWorkflow, }; }); const options = { extract: el => el.name }; return { list, options }; }; this.autocompleteSearch = async (_, input = '') => { try { const { list, options } = this.fuzzyFilterParams(); const fuzzyResult = fuzzy_1.default.filter(input, list, options); return fuzzyResult.map(result => result.original); } catch (err) { this.debug('%O', err); throw err; } }; this.selectOpOrWorkflowToRun = async (inputs) => { try { const { opsAndWorkflows } = inputs; if (!opsAndWorkflows || !opsAndWorkflows.length) throw new CustomErrors_1.InvalidOpName(); if (opsAndWorkflows.length === 1) { return Object.assign(Object.assign({}, inputs), { opOrWorkflow: opsAndWorkflows[0] }); } this.opsAndWorkflows = opsAndWorkflows; const { opOrWorkflow } = await this.ux.prompt({ type: 'autocomplete', name: 'opOrWorkflow', pageSize: 5, message: `\nSelect a ${this.ux.colors.multiBlue('\u2022Command')} or ${this.ux.colors.multiOrange('\u2022Workflow')} to run ${this.ux.colors.reset(this.ux.colors.green('→'))}\n${this.ux.colors.reset(this.ux.colors.dim('🌎 = Public 🔑 = Private 🖥 = Local 🔍 Search:'))} `, source: this.autocompleteSearch.bind(this), }); return Object.assign(Object.assign({}, inputs), { opOrWorkflow }); } catch (err) { this.debug('%O', err); throw err; } }; this.printCustomHelp = (op) => { try { if (!op.help) { throw new Error('Custom help message can be defined in the ops.yml\n'); } switch (true) { case Boolean(op.description): this.log(`\n${op.description}`); case Boolean(op.help.usage): this.log(`\n${this.ux.colors.bold('USAGE')}`); this.log(` ${op.help.usage}`); case Boolean(op.help.arguments): this.log(`\n${this.ux.colors.bold('ARGUMENTS')}`); Object.keys(op.help.arguments).forEach(a => { this.log(` ${a} ${this.ux.colors.dim(op.help.arguments[a])}`); }); case Boolean(op.help.options): this.log(`\n${this.ux.colors.bold('OPTIONS')}`); Object.keys(op.help.options).forEach(o => { this.log(` -${o.substring(0, 1)}, --${o} ${this.ux.colors.dim(op.help.options[o])}`); }); } } catch (err) { this.debug('%O', err); throw err; } }; this.checkForHelpMessage = (inputs) => { try { const { parsedArgs: { flags: { help }, }, opOrWorkflow, } = inputs; // TODO add support for workflows help if (help && 'run' in opOrWorkflow) { this.printCustomHelp(opOrWorkflow); process.exit(); } return inputs; } catch (err) { this.debug('%O', err); throw err; } }; this.executeOpOrWorkflowService = async (inputs) => { try { let { opOrWorkflow, config, parsedArgs, parsedArgs: { opParams }, teamName, opVersion, } = inputs; if (opOrWorkflow.type === opConfig_1.WORKFLOW_TYPE) { await this.services.workflowService.run(opOrWorkflow, opParams, config); } else { if (!opOrWorkflow.isPublished) { opOrWorkflow = Object.assign(Object.assign({}, opOrWorkflow), { isPublished: false, teamName: opOrWorkflow.teamName || teamName }); } await this.services.opService.run(opOrWorkflow, parsedArgs, config, opVersion); } return Object.assign(Object.assign({}, inputs), { opOrWorkflow }); } catch (err) { this.debug('%O', err); throw err; } }; /** * Extracts the Op Team and Name from the input argument * @cto.ai/github -> { teamName: cto.ai, opname: github, opVersion: '' } * cto.ai/github -> { teamName: cto.ai, opname: github, opVersion: '' } * github -> { teamName: '', opname: github, opVersion: '' } * @cto.ai/github:0.1.0 -> { teamName: cto.ai, opname: github, opVersion: '0.1.0' } * cto.ai/github:customVersion -> { teamName: cto.ai, opname: github, opVersion: 'customVersion' } * github:myVersion -> { teamName: '', opname: github, opVersion: 'myVersion' } * cto.ai/extra/blah -> InvalidOpName * cto.ai/extra:version1:version2 -> InvalidOpName * null -> InvalidOpName */ this.parseTeamOpNameVersion = (inputs) => { const { parsedArgs: { args: { nameOrPath }, }, config: { team: { name: configTeamName }, }, } = inputs; const splits = nameOrPath.split('/'); if (splits.length === 0 || splits.length > 2) throw new CustomErrors_1.InvalidOpName(); if (splits.length === 1) { let [opNameAndVersion] = splits; opNameAndVersion = splits[0]; const { opName, opVersion } = this.parseOpNameAndVersion(opNameAndVersion); if (!validate_1.isValidOpName(opName)) throw new CustomErrors_1.InvalidOpName(); return Object.assign(Object.assign({}, inputs), { teamName: configTeamName, opName, opVersion }); } let [teamName, opNameAndVersion] = splits; teamName = teamName.startsWith('@') ? teamName.substring(1, teamName.length) : teamName; teamName = validate_1.isValidTeamName(teamName) ? teamName : ''; const { opName, opVersion } = this.parseOpNameAndVersion(opNameAndVersion); if (!validate_1.isValidOpName(opName)) throw new CustomErrors_1.InvalidOpName(); return Object.assign(Object.assign({}, inputs), { teamName, opName, opVersion }); }; /** * Extracts the version and op name from input argument. * github -> { opName: 'github', opVersion: '' } * github:0.1.0 -> { opName: 'github', opVersion: '0.1.0' } * github:: -> InvalidOpName */ this.parseOpNameAndVersion = (opNameAndVersion) => { const splits = opNameAndVersion.split(':'); if (splits.length === 0 || splits.length > 2) throw new CustomErrors_1.InvalidOpName(); if (splits.length === 1) { return { opName: opNameAndVersion, opVersion: '', }; } const [opName, opVersion] = splits; return { opName, opVersion }; }; this.getApiOps = async (inputs) => { let { config, teamName, opName, opsAndWorkflows: previousOpsAndWorkflows = [], opVersion, } = inputs; let apiOp; try { if (!opName) return Object.assign({}, inputs); teamName = teamName ? teamName : config.team.name; if (opVersion) { ; ({ data: apiOp } = await this.services.api.find(`/private/teams/${teamName}/ops/${opName}/versions/${opVersion}`, { headers: { Authorization: this.accessToken, }, })); } else { ; ({ data: apiOp } = await this.services.api.find(`/private/teams/${teamName}/ops/${opName}`, { headers: { Authorization: this.accessToken, }, })); } } catch (err) { this.debug('%O', err); if (err.error[0].code === 404) { throw new CustomErrors_1.NoOpsFound(`@${teamName}/${opName}:${opVersion}`); } if (err.error[0].code === 4011) { throw new CustomErrors_1.UnauthorizedtoAccessOp(err); } if (err.error[0].code === 4033) { throw new CustomErrors_1.NoTeamFound(teamName); } throw new CustomErrors_1.APIError(err); } if (!apiOp && !previousOpsAndWorkflows.length) { throw new CustomErrors_1.NoOpsFound(opName, teamName); } if (!apiOp) { return Object.assign({}, inputs); } apiOp.isPublished = true; return Object.assign(Object.assign({}, inputs), { opsAndWorkflows: [...previousOpsAndWorkflows, apiOp] }); }; this.sendAnalytics = async (inputs) => { const { opOrWorkflow: { id, name, description, version }, parsedArgs: { opParams }, config: { user: { username, email }, team: { name: teamName, id: teamId }, }, } = inputs; this.services.analytics.track({ userId: email, teamId, cliEvent: 'Ops CLI Run', event: 'Ops CLI Run', properties: { name, team: teamName, email, username, namespace: `@${teamName}/${name}`, runtime: 'CLI', id, description, image: `${env_1.OPS_REGISTRY_HOST}/${name}:${version}`, argments: opParams.length, cliVersion: this.config.version, }, }, this.accessToken); return inputs; }; } async run() { try { await this.isLoggedIn(); const { config } = this.state; const parsedArgs = this.customParse(Run, this.argv); const { args: { nameOrPath }, } = parsedArgs; if (this.checkPathOpsYmlExists(nameOrPath)) { /* The nameOrPath argument is a directory containing an ops.yml */ const runFsPipeline = utils_1.asyncPipe(this.logResolvedLocalMessage, this.getOpsAndWorkflowsFromFileSystem(nameOrPath), this.addMissingApiFieldsToLocalOps, this.selectOpOrWorkflowToRun, this.checkForHelpMessage, this.sendAnalytics, this.executeOpOrWorkflowService); await runFsPipeline({ parsedArgs, config }); } else { /* * nameOrPath is either the name of an op and not a directory, or a * directory which does not contain an ops.yml. */ const runApiPipeline = utils_1.asyncPipe(this.getOpsAndWorkflowsFromFileSystem(process.cwd()), this.addMissingApiFieldsToLocalOps, this.filterLocalOps, this.parseTeamOpNameVersion, this.getApiOps, this.selectOpOrWorkflowToRun, this.checkForHelpMessage, this.sendAnalytics, this.executeOpOrWorkflowService); await runApiPipeline({ parsedArgs, config }); } } catch (err) { this.debug('%O', err); this.config.runHook('error', { err, accessToken: this.accessToken }); } } } exports.default = Run; Run.description = 'Run an Op from your team or the registry.'; Run.flags = { help: base_1.flags.boolean({ char: 'h', description: 'show CLI help', }), build: base_1.flags.boolean({ char: 'b', description: 'Builds the op before running. Must provide a path to the op.', default: false, }), }; // Used to specify variable length arguments Run.strict = false; Run.args = [ { name: 'nameOrPath', description: 'Name or path of the command or workflow you want to run.', parse: (input) => { return input.toLowerCase(); }, }, ];