@cto.ai/ops
Version:
💻 CTO.ai Ops - The CLI built for Teams 🚀
377 lines (376 loc) • 17.9 kB
JavaScript
"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();
},
},
];