1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const fuzzy_1 = tslib_1.__importDefault(require("fuzzy"));
|
5 | const fs = tslib_1.__importStar(require("fs-extra"));
|
6 | const path = tslib_1.__importStar(require("path"));
|
7 | const base_1 = tslib_1.__importStar(require("../base"));
|
8 | const CustomErrors_1 = require("../errors/CustomErrors");
|
9 | const opConfig_1 = require("../constants/opConfig");
|
10 | const env_1 = require("../constants/env");
|
11 | const utils_1 = require("../utils");
|
12 | const validate_1 = require("../utils/validate");
|
13 | class 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 |
|
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 |
|
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 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
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 |
|
240 |
|
241 |
|
242 |
|
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 |
|
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 |
|
341 |
|
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 | }
|
353 | exports.default = Run;
|
354 | Run.description = 'Run an Op from your team or the registry.';
|
355 | Run.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 |
|
367 | Run.strict = false;
|
368 | Run.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 | ];
|