UNPKG

14.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const sdk_1 = require("@cto.ai/sdk");
5const fuzzy_1 = tslib_1.__importDefault(require("fuzzy"));
6const fs = tslib_1.__importStar(require("fs-extra"));
7const path = tslib_1.__importStar(require("path"));
8const base_1 = tslib_1.__importStar(require("../base"));
9const CustomErrors_1 = require("../errors/CustomErrors");
10const opConfig_1 = require("../constants/opConfig");
11const env_1 = require("../constants/env");
12const utils_1 = require("../utils");
13const validate_1 = require("../utils/validate");
14const { multiBlue, multiOrange, green, dim, reset, bold } = sdk_1.ux.colors;
15class Run extends base_1.default {
16 constructor() {
17 super(...arguments);
18 this.opsAndWorkflows = [];
19 this.customParse = (options, argv) => {
20 const { args, flags } = require('@oclif/parser').parse(argv, Object.assign({}, options, { context: this }));
21 if (!args.nameOrPath && !flags.help) {
22 throw new CustomErrors_1.MissingRequiredArgument('ops run');
23 }
24 if (!args.nameOrPath)
25 this._help();
26 return { args, flags, opParams: argv.slice(1) };
27 };
28 this.checkPathOpsYmlExists = (nameOrPath) => {
29 const pathToOpsYml = path.join(path.resolve(nameOrPath), opConfig_1.OP_FILE);
30 return fs.existsSync(pathToOpsYml);
31 };
32 this.parseYamlFile = async (relativePathToOpsYml) => {
33 const opsYmlExists = this.checkPathOpsYmlExists(relativePathToOpsYml);
34 if (!opsYmlExists) {
35 return null;
36 }
37 const opsYml = await fs.readFile(path.join(path.resolve(relativePathToOpsYml), opConfig_1.OP_FILE), 'utf8');
38 const { ops = [], workflows = [], version = '1' } = (await utils_1.parseYaml(opsYml));
39 return { ops, workflows, version };
40 };
41 this.logResolvedLocalMessage = (inputs) => {
42 const { parsedArgs: { args: { nameOrPath }, }, } = inputs;
43 this.log(`❗️ ${this.ux.colors.callOutCyan(nameOrPath)} ${this.ux.colors.white('resolved to a local path and is running local Op.')} `);
44 return inputs;
45 };
46 /* get all the commands and workflows in an ops.yml that match the nameOrPath */
47 this.getOpsAndWorkflowsFromFileSystem = (relativePathToOpsYml) => async (inputs) => {
48 const yamlContents = await this.parseYamlFile(relativePathToOpsYml);
49 if (!yamlContents) {
50 return Object.assign({}, inputs, { opsAndWorkflows: [] });
51 }
52 const { ops, workflows, version } = yamlContents;
53 return Object.assign({}, inputs, { opsAndWorkflows: [...ops, ...workflows], version });
54 };
55 this.addMissingApiFieldsToLocalOps = async (inputs) => {
56 const { opsAndWorkflows, config } = inputs;
57 const updatedOpsAndWorkflows = opsAndWorkflows.map((opOrWorkflow) => {
58 let newOpOrWorkflow = Object.assign({}, opOrWorkflow);
59 newOpOrWorkflow.teamName = config.team.name;
60 newOpOrWorkflow.type =
61 'steps' in newOpOrWorkflow ? opConfig_1.WORKFLOW_TYPE : opConfig_1.COMMAND_TYPE;
62 return newOpOrWorkflow;
63 });
64 return Object.assign({}, inputs, { opsAndWorkflows: updatedOpsAndWorkflows });
65 };
66 this.filterLocalOps = (inputs) => {
67 const { opsAndWorkflows } = inputs;
68 if (!opsAndWorkflows) {
69 return Object.assign({}, inputs);
70 }
71 const { parsedArgs: { args: { nameOrPath }, }, } = inputs;
72 const keepOnlyMatchingNames = ({ name }) => {
73 return name.indexOf(nameOrPath) >= 0;
74 };
75 return Object.assign({}, inputs, { opsAndWorkflows: opsAndWorkflows.filter(keepOnlyMatchingNames) });
76 };
77 this.formatOpOrWorkflowEmoji = (opOrWorkflow) => {
78 if (!opOrWorkflow.isPublished) {
79 return '🖥 ';
80 }
81 else if (opOrWorkflow.isPublic) {
82 return '🌎 ';
83 }
84 else {
85 return '🔑 ';
86 }
87 };
88 this.formatOpOrWorkflowName = (opOrWorkflow) => {
89 const name = reset.white(opOrWorkflow.name);
90 if ((!opOrWorkflow.isPublished && 'steps' in opOrWorkflow) ||
91 (opOrWorkflow.isPublished && opOrWorkflow.type === opConfig_1.WORKFLOW_TYPE)) {
92 return `${reset(multiOrange('\u2022'))} ${this.formatOpOrWorkflowEmoji(opOrWorkflow)} ${name}`;
93 }
94 else {
95 return `${reset(multiBlue('\u2022'))} ${this.formatOpOrWorkflowEmoji(opOrWorkflow)} ${name}`;
96 }
97 };
98 this.fuzzyFilterParams = () => {
99 const list = this.opsAndWorkflows.map(opOrWorkflow => {
100 const name = this.formatOpOrWorkflowName(opOrWorkflow);
101 return {
102 name: `${name} - ${opOrWorkflow.description}`,
103 value: opOrWorkflow,
104 };
105 });
106 const options = { extract: el => el.name };
107 return { list, options };
108 };
109 this.autocompleteSearch = async (_, input = '') => {
110 try {
111 const { list, options } = this.fuzzyFilterParams();
112 const fuzzyResult = fuzzy_1.default.filter(input, list, options);
113 return fuzzyResult.map(result => result.original);
114 }
115 catch (err) {
116 this.debug('%O', err);
117 throw err;
118 }
119 };
120 this.selectOpOrWorkflowToRun = async (inputs) => {
121 try {
122 const { opsAndWorkflows } = inputs;
123 if (!opsAndWorkflows || !opsAndWorkflows.length)
124 throw new CustomErrors_1.InvalidOpName();
125 if (opsAndWorkflows.length === 1) {
126 return Object.assign({}, inputs, { opOrWorkflow: opsAndWorkflows[0] });
127 }
128 this.opsAndWorkflows = opsAndWorkflows;
129 const { opOrWorkflow } = await sdk_1.ux.prompt({
130 type: 'autocomplete',
131 name: 'opOrWorkflow',
132 pageSize: 5,
133 message: `\nSelect a ${multiBlue('\u2022Command')} or ${multiOrange('\u2022Workflow')} to run ${reset(green('→'))}\n${reset(dim('🌎 = Public 🔑 = Private 🖥 = Local 🔍 Search:'))} `,
134 source: this.autocompleteSearch.bind(this),
135 });
136 return Object.assign({}, inputs, { opOrWorkflow });
137 }
138 catch (err) {
139 this.debug('%O', err);
140 throw err;
141 }
142 };
143 this.printCustomHelp = (op) => {
144 try {
145 if (!op.help) {
146 throw new Error('Custom help message can be defined in the ops.yml\n');
147 }
148 switch (true) {
149 case Boolean(op.description):
150 this.log(`\n${op.description}`);
151 case Boolean(op.help.usage):
152 this.log(`\n${bold('USAGE')}`);
153 this.log(` ${op.help.usage}`);
154 case Boolean(op.help.arguments):
155 this.log(`\n${bold('ARGUMENTS')}`);
156 Object.keys(op.help.arguments).forEach(a => {
157 this.log(` ${a} ${dim(op.help.arguments[a])}`);
158 });
159 case Boolean(op.help.options):
160 this.log(`\n${bold('OPTIONS')}`);
161 Object.keys(op.help.options).forEach(o => {
162 this.log(` -${o.substring(0, 1)}, --${o} ${dim(op.help.options[o])}`);
163 });
164 }
165 }
166 catch (err) {
167 this.debug('%O', err);
168 throw err;
169 }
170 };
171 this.checkForHelpMessage = (inputs) => {
172 try {
173 const { parsedArgs: { flags: { help }, }, opOrWorkflow, } = inputs;
174 // TODO add support for workflows help
175 if (help && 'run' in opOrWorkflow) {
176 this.printCustomHelp(opOrWorkflow);
177 process.exit();
178 }
179 return inputs;
180 }
181 catch (err) {
182 this.debug('%O', err);
183 throw err;
184 }
185 };
186 this.executeOpOrWorkflowService = async (inputs) => {
187 try {
188 let { opOrWorkflow, config, parsedArgs, parsedArgs: { opParams }, teamName, version, } = inputs;
189 if (opOrWorkflow.type === opConfig_1.WORKFLOW_TYPE) {
190 await this.services.workflowService.run(opOrWorkflow, opParams, config);
191 }
192 else {
193 if (!opOrWorkflow.isPublished) {
194 opOrWorkflow = Object.assign({}, opOrWorkflow, { isPublished: false, teamName: opOrWorkflow.teamName || teamName });
195 }
196 await this.services.opService.run(opOrWorkflow, parsedArgs, config, version);
197 }
198 return Object.assign({}, inputs, { opOrWorkflow });
199 }
200 catch (err) {
201 this.debug('%O', err);
202 throw err;
203 }
204 };
205 /**
206 * Extracts the Op Team and Name from the input argument
207 * @cto.ai/github -> { teamName: cto.ai, opname: github }
208 * cto.ai/github -> { teamName: cto.ai, opname: github }
209 * github -> { teamName: '', opname: github }
210 * cto.ai/extra/blah -> InvalidOpName
211 * null -> InvalidOpName
212 */
213 this.parseTeamAndOpName = (inputs) => {
214 const { parsedArgs: { args: { nameOrPath }, }, config: { team: { name: configTeamName }, }, } = inputs;
215 const splits = nameOrPath.split('/');
216 if (splits.length === 0 || splits.length > 2)
217 throw new CustomErrors_1.InvalidOpName();
218 if (splits.length === 1) {
219 let [opName] = splits;
220 opName = validate_1.isValidOpName(splits[0]) ? splits[0] : '';
221 return Object.assign({}, inputs, { teamName: configTeamName, opName });
222 }
223 let [teamName, opName] = splits;
224 teamName = teamName.startsWith('@')
225 ? teamName.substring(1, teamName.length)
226 : teamName;
227 teamName = validate_1.isValidTeamName(teamName) ? teamName : '';
228 opName = validate_1.isValidOpName(opName) ? opName : '';
229 return Object.assign({}, inputs, { teamName, opName });
230 };
231 this.getApiOps = async (inputs) => {
232 let { config, teamName, opName, opsAndWorkflows: previousOpsAndWorkflows = [], } = inputs;
233 let apiOp;
234 try {
235 if (!opName)
236 return Object.assign({}, inputs);
237 teamName = teamName ? teamName : config.team.name;
238 ({ data: apiOp } = await this.services.api.find(`teams/${teamName}/ops/${opName}`, {
239 headers: {
240 Authorization: this.accessToken,
241 },
242 }));
243 }
244 catch (err) {
245 this.debug('%O', err);
246 if (err.error[0].code === 4011) {
247 throw new CustomErrors_1.UnauthorizedtoAccessOp(err);
248 }
249 throw new CustomErrors_1.APIError(err);
250 }
251 if (!apiOp) {
252 throw new CustomErrors_1.NoOpsFound(opName);
253 }
254 apiOp.isPublished = true;
255 return Object.assign({}, inputs, { opsAndWorkflows: [...previousOpsAndWorkflows, apiOp] });
256 };
257 this.sendAnalytics = (inputs) => {
258 const { opOrWorkflow: { id, name, description }, parsedArgs: { opParams }, } = inputs;
259 this.services.analytics.track({
260 userId: this.user.email,
261 event: 'Ops CLI Run',
262 properties: {
263 email: this.user.email,
264 username: this.user.username,
265 id,
266 name,
267 description,
268 argments: opParams.length,
269 image: `${env_1.OPS_REGISTRY_HOST}/${name}`,
270 },
271 }, this.accessToken);
272 return inputs;
273 };
274 }
275 async run() {
276 try {
277 await this.isLoggedIn();
278 const { config } = this.state;
279 const parsedArgs = this.customParse(Run, this.argv);
280 const { args: { nameOrPath }, } = parsedArgs;
281 if (this.checkPathOpsYmlExists(nameOrPath)) {
282 /* The nameOrPath argument is a directory containing an ops.yml */
283 const runFsPipeline = utils_1.asyncPipe(this.logResolvedLocalMessage, this.getOpsAndWorkflowsFromFileSystem(nameOrPath), this.addMissingApiFieldsToLocalOps, this.selectOpOrWorkflowToRun, this.checkForHelpMessage, this.sendAnalytics, this.executeOpOrWorkflowService);
284 await runFsPipeline({ parsedArgs, config });
285 }
286 else {
287 /*
288 * nameOrPath is either the name of an op and not a directory, or a
289 * directory which does not contain an ops.yml.
290 */
291 const runApiPipeline = utils_1.asyncPipe(this.getOpsAndWorkflowsFromFileSystem(process.cwd()), this.addMissingApiFieldsToLocalOps, this.filterLocalOps, this.parseTeamAndOpName, this.getApiOps, this.selectOpOrWorkflowToRun, this.checkForHelpMessage, this.sendAnalytics, this.executeOpOrWorkflowService);
292 await runApiPipeline({ parsedArgs, config });
293 }
294 }
295 catch (err) {
296 this.debug('%O', err);
297 this.config.runHook('error', { err, accessToken: this.accessToken });
298 }
299 }
300}
301Run.description = 'Run an op from the registry.';
302Run.flags = {
303 help: base_1.flags.boolean({
304 char: 'h',
305 description: 'show CLI help',
306 }),
307 build: base_1.flags.boolean({
308 char: 'b',
309 description: 'Builds the op before running. Must provide a path to the op.',
310 default: false,
311 }),
312};
313// Used to specify variable length arguments
314Run.strict = false;
315Run.args = [
316 {
317 name: 'nameOrPath',
318 description: 'Name or path of the command or workflow you want to run.',
319 },
320];
321exports.default = Run;