UNPKG

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