UNPKG

17.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const fs = tslib_1.__importStar(require("fs-extra"));
5const path = tslib_1.__importStar(require("path"));
6const yaml = tslib_1.__importStar(require("yaml"));
7const base_1 = tslib_1.__importStar(require("../base"));
8const asyncPipe_1 = require("../utils/asyncPipe");
9const CustomErrors_1 = require("../errors/CustomErrors");
10const opConfig_1 = require("../constants/opConfig");
11const utils_1 = require("../utils");
12const utils_2 = require("../utils");
13class Init extends base_1.default {
14 constructor() {
15 super(...arguments);
16 this.questions = [];
17 this.srcDir = path.resolve(__dirname, '../templates/');
18 this.destDir = path.resolve(process.cwd());
19 this.initPrompts = {
20 [utils_1.appendSuffix(opConfig_1.COMMAND, 'Name')]: {
21 type: 'input',
22 name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Name'),
23 message: `\n Provide a name for your new command ${this.ux.colors.reset.green('ā†’')}\n${this.ux.colors.reset(this.ux.colors.secondary('Names must be lowercase'))}\n\nšŸ· ${this.ux.colors.white('Name:')}`,
24 afterMessage: this.ux.colors.reset.green('āœ“'),
25 afterMessageAppend: this.ux.colors.reset(' added!'),
26 validate: this._validateName,
27 transformer: input => this.ux.colors.cyan(input.toLocaleLowerCase()),
28 filter: input => input.toLowerCase(),
29 },
30 [utils_1.appendSuffix(opConfig_1.COMMAND, 'Description')]: {
31 type: 'input',
32 name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Description'),
33 message: `\nProvide a description ${this.ux.colors.reset.green('ā†’')} \nāœļø ${this.ux.colors.white('Description:')}`,
34 afterMessage: this.ux.colors.reset.green('āœ“'),
35 afterMessageAppend: this.ux.colors.reset(' added!'),
36 validate: this._validateDescription,
37 },
38 [utils_1.appendSuffix(opConfig_1.COMMAND, 'Version')]: {
39 type: 'input',
40 name: utils_1.appendSuffix(opConfig_1.COMMAND, 'Version'),
41 message: `\nProvide a version ${this.ux.colors.reset.green('ā†’')} \nāœļø ${this.ux.colors.white('Version:')}`,
42 afterMessage: this.ux.colors.reset.green('āœ“'),
43 afterMessageAppend: this.ux.colors.reset(' added!'),
44 validate: this._validateVersion,
45 default: '0.1.0',
46 },
47 [utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Name')]: {
48 type: 'input',
49 name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Name'),
50 message: `\n Provide a name for your new workflow ${this.ux.colors.reset.green('ā†’')}\n${this.ux.colors.reset(this.ux.colors.secondary('Names must be lowercase'))}\n\nšŸ· ${this.ux.colors.white('Name:')}`,
51 afterMessage: this.ux.colors.reset.green('āœ“'),
52 afterMessageAppend: this.ux.colors.reset(' added!'),
53 validate: this._validateName,
54 transformer: input => this.ux.colors.cyan(input.toLocaleLowerCase()),
55 filter: input => input.toLowerCase(),
56 },
57 [utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Description')]: {
58 type: 'input',
59 name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Description'),
60 message: `\nProvide a description ${this.ux.colors.reset.green('ā†’')}\n\nāœļø ${this.ux.colors.white('Description:')}`,
61 afterMessage: this.ux.colors.reset.green('āœ“'),
62 afterMessageAppend: this.ux.colors.reset(' added!'),
63 validate: this._validateDescription,
64 },
65 [utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Version')]: {
66 type: 'input',
67 name: utils_1.appendSuffix(opConfig_1.WORKFLOW, 'Version'),
68 message: `\nProvide a version ${this.ux.colors.reset.green('ā†’')}\n\nāœļø ${this.ux.colors.white('Version:')}`,
69 afterMessage: this.ux.colors.reset.green('āœ“'),
70 afterMessageAppend: this.ux.colors.reset(' added!'),
71 validate: this._validateVersion,
72 default: '0.1.0',
73 },
74 };
75 this.determineTemplate = async (prompts) => {
76 const { templates } = await this.ux.prompt({
77 type: 'checkbox',
78 name: 'templates',
79 message: `What type of op would you like to create ${this.ux.colors.reset.green('ā†’')}`,
80 choices: [
81 {
82 name: `${utils_1.titleCase(opConfig_1.COMMAND)} - A template for building commands which can be distributed via The Ops Platform.`,
83 value: opConfig_1.COMMAND,
84 },
85 {
86 name: `${utils_1.titleCase(opConfig_1.WORKFLOW)} - A template for combining many commands into a workflow which can be distributed via The Ops Platform.`,
87 value: opConfig_1.WORKFLOW,
88 },
89 ],
90 afterMessage: `${this.ux.colors.reset.green('āœ“')}`,
91 validate: input => input.length != 0,
92 });
93 return { prompts, templates };
94 };
95 this.determineQuestions = ({ prompts, templates, }) => {
96 // Filters initPrompts based on the templates selected in determineTemplate
97 const removeIfNotSelectedTemplate = ([key, _val]) => {
98 return key.includes(templates[0]) || key.includes(templates[1]);
99 };
100 const questions = Object.entries(prompts)
101 .filter(removeIfNotSelectedTemplate)
102 .map(([_key, question]) => question);
103 return { questions, templates };
104 };
105 this.askQuestions = async ({ questions, templates, }) => {
106 const answers = await this.ux.prompt(questions);
107 return { answers, templates };
108 };
109 this.determineInitPaths = ({ answers, templates, }) => {
110 const initParams = Object.assign(Object.assign({}, answers), { templates });
111 const { name } = this.getNameAndDescription(initParams);
112 const sharedDir = `${this.srcDir}/shared`;
113 const destDir = `${this.destDir}/${name}`;
114 const initPaths = { sharedDir, destDir };
115 return { initPaths, initParams };
116 };
117 this.copyTemplateFiles = async ({ initPaths, initParams, }) => {
118 try {
119 const { templates } = initParams;
120 const { destDir, sharedDir } = initPaths;
121 await fs.ensureDir(destDir);
122 await fs.copy(sharedDir, destDir);
123 return { initPaths, initParams };
124 }
125 catch (err) {
126 this.debug('%O', err);
127 throw new CustomErrors_1.CopyTemplateFilesError(err);
128 }
129 };
130 this.customizePackageJson = async ({ initPaths, initParams, }) => {
131 try {
132 const { destDir, sharedDir } = initPaths;
133 const { name, description } = this.getNameAndDescription(initParams);
134 const packageObj = JSON.parse(fs.readFileSync(`${sharedDir}/package.json`, 'utf8'));
135 packageObj.name = name;
136 packageObj.description = description;
137 const newPackageString = JSON.stringify(packageObj, null, 2);
138 fs.writeFileSync(`${destDir}/package.json`, newPackageString);
139 return { initPaths, initParams };
140 }
141 catch (err) {
142 this.debug('%O', err);
143 throw new CustomErrors_1.CouldNotInitializeOp(err);
144 }
145 };
146 this.customizeYaml = async ({ initPaths, initParams, }) => {
147 try {
148 const { destDir } = initPaths;
149 // Parse YAML as document so we can work with comments
150 const opsYamlDoc = yaml.parseDocument(fs.readFileSync(`${destDir}/ops.yml`, 'utf8'));
151 await this.customizeOpsYaml(initParams, opsYamlDoc);
152 await this.customizeWorkflowYaml(initParams, opsYamlDoc);
153 // Process each root level section of the YAML file & add comments
154 Object.keys(opConfig_1.HELP_COMMENTS).forEach(rootKey => {
155 this.addHelpCommentsFor(rootKey, opsYamlDoc);
156 });
157 // Get the YAML file as string
158 const newOpsString = opsYamlDoc.toString();
159 fs.writeFileSync(`${destDir}/ops.yml`, newOpsString);
160 return { initPaths, initParams };
161 }
162 catch (err) {
163 this.debug('%O', err);
164 throw new CustomErrors_1.CouldNotInitializeOp(err);
165 }
166 };
167 // The `yaml` library has a pretty bad API for handling comments
168 // More: https://eemeli.org/yaml/#comments'
169 // TODO: Review type checking for yamlDoc (yaml.ast.Document) & remove tsignores
170 this.addHelpCommentsFor = (key, yamlDoc) => {
171 const docContents = yamlDoc.contents;
172 const docContentsItems = docContents.items;
173 const configItem = docContentsItems.find(item => {
174 if (!item || !item.key)
175 return;
176 const itemKey = item.key;
177 return itemKey.value === key;
178 });
179 // Simple config fields (`version`)
180 if (configItem &&
181 configItem.value &&
182 configItem.value.type === opConfig_1.YAML_TYPE_STRING &&
183 opConfig_1.HELP_COMMENTS[key]) {
184 configItem.comment = ` ${opConfig_1.HELP_COMMENTS[key]}`;
185 }
186 // Config fields with nested values (`ops`, `workflows`)
187 if (configItem &&
188 configItem.value &&
189 configItem.value.type === opConfig_1.YAML_TYPE_SEQUENCE) {
190 // @ts-ignore
191 yamlDoc.getIn([key, 0]).items.map(configItem => {
192 const comment = opConfig_1.HELP_COMMENTS[key][configItem.key];
193 if (comment)
194 configItem.comment = ` ${opConfig_1.HELP_COMMENTS[key][configItem.key]}`;
195 });
196 }
197 };
198 this.customizeOpsYaml = async (initParams, yamlDoc) => {
199 const { templates, commandName, commandDescription, commandVersion, } = initParams;
200 if (!templates.includes(opConfig_1.COMMAND)) {
201 // @ts-ignore
202 yamlDoc.delete('commands');
203 return;
204 }
205 yamlDoc
206 // @ts-ignore
207 .getIn(['commands', 0])
208 .set('name', `${commandName}:${commandVersion}`);
209 // @ts-ignore
210 yamlDoc.getIn(['commands', 0]).set('description', commandDescription);
211 };
212 this.customizeWorkflowYaml = async (initParams, yamlDoc) => {
213 const { templates, workflowName, workflowDescription, workflowVersion, } = initParams;
214 if (!templates.includes(opConfig_1.WORKFLOW)) {
215 // @ts-ignore
216 yamlDoc.delete('workflows');
217 return;
218 }
219 yamlDoc
220 // @ts-ignore
221 .getIn(['workflows', 0])
222 .set('name', `${workflowName}:${workflowVersion}`);
223 // @ts-ignore
224 yamlDoc.getIn(['workflows', 0]).set('description', workflowDescription);
225 };
226 this.logMessages = async ({ initPaths, initParams, }) => {
227 const { destDir } = initPaths;
228 const { templates } = initParams;
229 const { name } = this.getNameAndDescription(initParams);
230 this.logSuccessMessage(templates);
231 fs.readdirSync(`${destDir}`).forEach((file) => {
232 let callout = '';
233 if (file.indexOf('index.js') > -1) {
234 callout = `${this.ux.colors.green('ā†')} ${this.ux.colors.white('Start developing here!')}`;
235 }
236 let msg = this.ux.colors.italic(`${path.relative(this.destDir, process.cwd())}/${name}/${file} ${callout}`);
237 this.log(`šŸ“ .${msg}`);
238 });
239 if (templates.includes(opConfig_1.COMMAND)) {
240 this.logCommandMessage(initParams);
241 }
242 if (templates.includes(opConfig_1.WORKFLOW)) {
243 this.logWorkflowMessage(initParams);
244 }
245 return { initPaths, initParams };
246 };
247 this.logCommandMessage = (initParams) => {
248 const { commandName } = initParams;
249 this.log(`\nšŸš€ To test your ${opConfig_1.COMMAND} run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`ops run ${commandName}`)}`);
250 };
251 this.logWorkflowMessage = (initParams) => {
252 const { name } = this.getNameAndDescription(initParams);
253 this.log(`\nšŸš€ To test your ${opConfig_1.WORKFLOW} run: ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan(`cd ${name} && npm install && ops run .`)}`);
254 };
255 this.logSuccessMessage = (templates) => {
256 const successMessageBoth = `\nšŸŽ‰ Success! Your ${opConfig_1.COMMAND} and ${opConfig_1.WORKFLOW} template Ops are ready to start coding... \n`;
257 const getSuccessMessage = (opType) => `\nšŸŽ‰ Success! Your ${opType} template Op is ready to start coding... \n`;
258 if (templates.includes(opConfig_1.COMMAND) && templates.includes(opConfig_1.WORKFLOW)) {
259 return this.log(successMessageBoth);
260 }
261 const opType = templates.includes(opConfig_1.COMMAND) ? opConfig_1.COMMAND : opConfig_1.WORKFLOW;
262 return this.log(getSuccessMessage(opType));
263 };
264 this.sendAnalytics = async ({ initPaths, initParams, }) => {
265 try {
266 const { destDir } = initPaths;
267 const { templates } = initParams;
268 const { name, description } = this.getNameAndDescription(initParams);
269 this.services.analytics.track({
270 userId: this.user.email,
271 teamId: this.team.id,
272 cliEvent: 'Ops CLI Init',
273 event: 'Ops CLI Init',
274 properties: {
275 name,
276 team: this.team.name,
277 namespace: `@${this.team.name}/${name}`,
278 runtime: 'CLI',
279 email: this.user.email,
280 username: this.user.username,
281 path: destDir,
282 description,
283 templates,
284 },
285 }, this.accessToken);
286 return {
287 initPaths,
288 initParams,
289 };
290 }
291 catch (err) {
292 this.debug('%O', err);
293 throw new CustomErrors_1.AnalyticsError(err);
294 }
295 };
296 this.getNameAndDescription = (initParams) => {
297 return {
298 name: initParams.commandName || initParams.workflowName,
299 description: initParams.commandDescription || initParams.workflowDescription,
300 };
301 };
302 }
303 _validateName(input) {
304 if (input === '')
305 return 'You need name your op before you can continue';
306 if (!input.match('^[a-z0-9_-]*$')) {
307 return 'Sorry, please name the Op using only numbers, letters, -, or _';
308 }
309 return true;
310 }
311 _validateDescription(input) {
312 if (input === '')
313 return 'You need to provide a description of your op before continuing';
314 return true;
315 }
316 _validateVersion(input) {
317 if (input === '')
318 return 'You need to provide a version of your op before continuing';
319 if (!input.match(utils_2.validVersionChars)) {
320 return `Sorry, version can only contain letters, digits, underscores, periods and dashes\nand must start and end with a letter or a digit`;
321 }
322 return true;
323 }
324 async run() {
325 this.parse(Init);
326 try {
327 await this.isLoggedIn();
328 const initPipeline = asyncPipe_1.asyncPipe(this.determineTemplate, this.determineQuestions, this.askQuestions, this.determineInitPaths, this.copyTemplateFiles, this.customizePackageJson, this.customizeYaml, this.sendAnalytics, this.logMessages);
329 await initPipeline(this.initPrompts);
330 }
331 catch (err) {
332 this.debug('%O', err);
333 this.config.runHook('error', { err, accessToken: this.accessToken });
334 }
335 }
336}
337exports.default = Init;
338Init.description = 'Easily create a new Op.';
339Init.flags = {
340 help: base_1.flags.help({ char: 'h' }),
341};