UNPKG

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