UNPKG

9.62 kBJavaScriptView Raw
1const os = require('os');
2const path = require('path');
3const semver = require('semver');
4
5const httpClient = require('@src/clients/http-client');
6const {
7 validateRequiredOption,
8 validateOptionString,
9 validateOptionRules,
10} = require('@src/commands/option-validator');
11const AppConfig = require('@src/model/app-config');
12const CONSTANTS = require('@src/utils/constants');
13const Messenger = require('@src/view/messenger');
14const metricClient = require('@src/utils/metrics');
15const profileHelper = require('@src/utils/profile-helper');
16
17
18const packageJson = require('@root/package.json');
19
20/**
21 * Base class for ASK CLI command that provides option parsing, commander configuration and option validation at runtime.
22 */
23class AbstractCommand {
24 constructor(optionModel) {
25 // eslint-disable-next-line global-require
26 this._optionModel = optionModel || require('@src/commands/option-model');
27 }
28
29 name() {
30 throw new Error('Unimplemented abstract function: name()!');
31 }
32
33 description() {
34 throw new Error('Unimplemented abstract function: description()!');
35 }
36
37 requiredOptions() {
38 return [];
39 }
40
41 optionalOptions() {
42 return [];
43 }
44
45 handle() {}
46
47 exit(statusCode) {
48 Messenger.getInstance().dispose();
49 process.exit(statusCode || 0);
50 }
51
52 createCommand() {
53 new Messenger({});
54 return (commander) => {
55 try {
56 // register command name and description
57 const commanderCopy = commander
58 .command(this.name())
59 .description(this.description());
60
61 // register command options
62 this._registerOptions(commanderCopy);
63
64 // register command action
65 this._registerAction(commanderCopy);
66 } catch (err) {
67 Messenger.getInstance().fatal(err);
68 this.exit(1);
69 }
70 };
71 }
72
73 _registerAction(commander) {
74 // the result commander's parse, args, contains a Commander object (args[0]), and an array of unrecognized user inputs (args[1])
75 commander.action((...args) => new Promise((resolve) => {
76 const commandInstance = args[0];
77 const remaining = args[1];
78
79 // set Messenger debug preferrance
80 Messenger.getInstance().doDebug = commandInstance.debug;
81
82 // Start metric client
83 metricClient.startAction(commandInstance._name, 'command');
84
85 // Check if a new CLI version is released
86 this._remindsIfNewVersion(commandInstance.debug, process.env.ASK_SKIP_NEW_VERSION_REMINDER, () => {
87 try {
88 this._validateOptions(commandInstance);
89
90 /**
91 * Since this code is ran for every command, we'll just be initiating appConfig here (no files created).
92 * Only `ask configure` command should have the eligibility to create the ASK config file (which is handled
93 * in the configure workflow).
94 */
95 if (commandInstance._name !== 'configure'
96 && profileHelper.runtimeProfile(commandInstance.profile) !== CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.PROFILE_NAME) {
97 this._initiateAppConfig();
98 }
99 } catch (err) {
100 Messenger.getInstance().error(err);
101 resolve();
102 this.exit(1);
103 return;
104 }
105 // execute handler logic of each command; quit execution
106 this.handle(commandInstance, (error) => {
107 metricClient.sendData(error).then(() => {
108 resolve();
109 this.exit(error ? 1 : 0);
110 });
111 }, remaining);
112 });
113 }));
114 }
115
116 _registerOptions(commander) {
117 const requiredOptions = this.requiredOptions();
118 if (requiredOptions && requiredOptions.length) {
119 for (const optionId of requiredOptions) {
120 commander = this._registerOption(commander, optionId, true);
121 }
122 }
123
124 const optionalOptions = this.optionalOptions();
125 if (optionalOptions && optionalOptions.length) {
126 for (const optionId of optionalOptions) {
127 commander = this._registerOption(commander, optionId, false);
128 }
129 }
130
131 return commander;
132 }
133
134 _registerOption(commander, optionId, required) {
135 const optionModel = this._optionModel[optionId];
136
137 // Check if given option name has a model defined. Refer to option-model.json for all available option models
138 if (!optionModel) {
139 throw new Error(`Unrecognized option ID: ${optionId}`);
140 }
141
142 return commander.option(
143 AbstractCommand.buildOptionString(optionModel),
144 `${required ? '[REQUIRED]' : '[OPTIONAL]'} ${optionModel.description}`
145 );
146 }
147
148 _validateOptions(cmd) {
149 const requiredOptions = this.requiredOptions();
150 if (requiredOptions && requiredOptions.length) {
151 for (const optionId of requiredOptions) {
152 this._validateOption(cmd, optionId, true);
153 }
154 }
155
156 const optionalOptions = this.optionalOptions();
157 if (optionalOptions && optionalOptions.length) {
158 for (const optionId of optionalOptions) {
159 this._validateOption(cmd, optionId, false);
160 }
161 }
162 }
163
164 _validateOption(cmd, optionId, required) {
165 const optionModel = this._optionModel[optionId];
166 const optionKey = AbstractCommand.parseOptionKey(optionModel.name);
167 try {
168 if (required) {
169 validateRequiredOption(cmd, optionKey);
170 }
171
172 if (cmd[optionKey]) {
173 // Validate string value for options that require string input
174 if (optionModel.stringInput === 'REQUIRED') {
175 validateOptionString(cmd, optionKey);
176 }
177
178 validateOptionRules(cmd, optionKey, optionModel.rule);
179 }
180 } catch (err) {
181 throw (`Please provide valid input for option: ${optionModel.name}. ${err}`);
182 }
183 }
184
185 _remindsIfNewVersion(doDebug, skip, callback) {
186 if (skip) return callback();
187
188 httpClient.request({
189 url: `${CONSTANTS.NPM_REGISTRY_URL_BASE}/${CONSTANTS.APPLICATION_NAME}/latest`,
190 method: CONSTANTS.HTTP_REQUEST.VERB.GET
191 }, 'GET_NPM_REGISTRY', doDebug, (err, response) => {
192 if (err || response.statusCode > 300) {
193 const error = err || `Http Status Code: ${response.statusCode}.`;
194 Messenger.getInstance().error(`Failed to get the latest version for ${CONSTANTS.APPLICATION_NAME} from NPM registry.\n${error}\n`);
195 } else {
196 const BANNER_WITH_HASH = '##########################################################################';
197 const latestVersion = JSON.parse(response.body).version;
198 if (packageJson.version !== latestVersion) {
199 if (semver.major(packageJson.version) < semver.major(latestVersion)) {
200 Messenger.getInstance().info(`\
201${BANNER_WITH_HASH}
202[Info]: New MAJOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}.
203It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}".
204${BANNER_WITH_HASH}\n`);
205 } else if (
206 semver.major(packageJson.version) === semver.major(latestVersion)
207 && semver.minor(packageJson.version) < semver.minor(latestVersion)
208 ) {
209 Messenger.getInstance().info(`\
210${BANNER_WITH_HASH}
211[Info]: New MINOR version (v${latestVersion}) of ${CONSTANTS.APPLICATION_NAME} is available now. Current version v${packageJson.version}.
212It is recommended to use the latest version. Please update using "npm upgrade -g ${CONSTANTS.APPLICATION_NAME}".
213${BANNER_WITH_HASH}\n`);
214 }
215 }
216 }
217 callback();
218 });
219 }
220
221 _initiateAppConfig() {
222 const configFilePath = path.join(os.homedir(), CONSTANTS.FILE_PATH.ASK.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.ASK.PROFILE_FILE);
223 new AppConfig(configFilePath);
224 }
225
226 /**
227 * Build the usage string for an option
228 * @param {Object} optionModel
229 */
230 static buildOptionString(optionModel) {
231 const optionStringArray = [];
232
233 if (optionModel.alias) {
234 optionStringArray.push(`-${optionModel.alias},`);
235 }
236
237 optionStringArray.push(`--${optionModel.name}`);
238
239 if (optionModel.stringInput === 'REQUIRED') {
240 optionStringArray.push(`<${optionModel.name}>`);
241 } else if (optionModel.stringInput === 'OPTIONAL') {
242 optionStringArray.push(`[${optionModel.name}]`);
243 }
244
245 return optionStringArray.join(' ');
246 }
247
248 /**
249 * convert option name to option key
250 * Example: skill-id -> skillId
251 * @param name
252 */
253 static parseOptionKey(name) {
254 const arr = name.split('-');
255
256 return arr.slice(1).reduce((end, element) => end + element.charAt(0).toUpperCase() + element.slice(1), arr[0]);
257 }
258}
259
260module.exports = {
261 AbstractCommand
262};