-
-
Notifications
You must be signed in to change notification settings - Fork 227
/
command.js
256 lines (219 loc) · 8.38 KB
/
command.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
* Inspired by the Denali-CLI command class
* https://github.com/denali-js/denali-cli/blob/master/lib/command.ts
*/
const each = require('lodash/each');
const createDebug = require('debug');
const kebabCase = require('lodash/kebabCase');
const UI = require('./ui');
const System = require('./system');
const debug = createDebug('ghost-cli:command');
/**
* Base Command class. All commands, both internal and external,
* MUST extend this class
*
* @class Command
*/
class Command {
/**
* Configures the args/options for this command
*
* @param {String} commandName
* @param {Array<String>} aliases Aliases from abbrev
* @param {Yargs} yargs Yargs instance
* @param {Array} extensions Array of discovered extensions
* @return {Yargs} Yargs instance after command has been called
*
* @static
* @method configure
* @private
*/
static configure(commandName, aliases, yargs, extensions) {
if (!this.description) {
throw new Error(`Command ${commandName} must have a description!`);
}
debug(`adding configuration for ${commandName}`);
let command = commandName;
if (this.params) {
command += ` ${this.params}`;
}
return yargs.command({
command: command,
aliases: aliases,
describe: this.description,
builder: (commandArgs) => {
debug(`building options for ${commandName}`);
commandArgs = this.configureOptions(commandName, commandArgs, extensions);
if (this.configureSubcommands) {
commandArgs = this.configureSubcommands(commandName, commandArgs, extensions);
}
return commandArgs;
},
handler: (argv) => {
this._run(commandName, argv, extensions);
}
});
}
/**
* Configure Yargs for this command. Subclasses can override this to do anything
* specific that they need to do at config time
*
* @param {String} commandName Name of the command
* @param {Yargs} yargs Yargs instance
* @param {Array} extensions Array of discovered extensions
* @return {Yargs} Yargs instance after options are configured
*
* @static
* @method configureOptions
* @public
*/
static configureOptions(commandName, yargs, extensions, onlyOptions) {
each(this.options || {}, (option, optionName) => {
yargs = yargs.option(kebabCase(optionName), option);
});
if (onlyOptions) {
return yargs;
}
if (this.longDescription) {
yargs.usage(this.longDescription);
}
yargs.epilogue('For more information, see our docs at https://ghost.org/docs/ghost-cli/');
return yargs;
}
/**
* Actually runs the command
*
* @param {String} commandName Command Name
* @param {Object} argv Parsed arguments
* @param {Array} extensions Array of discovered extensions
* @param {Object} context Various contextual dependencies
*
* @static
* @method run
* @private
*/
static async _run(commandName, argv = {}, extensions) {
debug('running command prep');
// Set process title
process.title = `ghost ${commandName}`;
// Create CLI-wide UI & System instances
const ui = new UI({
verbose: argv.verbose,
allowPrompt: argv.prompt,
auto: argv.auto
});
// This needs to run before the installation check
if (argv.dir) {
if (argv.dir === true) {
// CASE: the short-form dir flag was provided, and a development flag was not provided in any form
// --> This is probably a typo
const help = process.argv.includes('-d') && !('development' in argv)
? '. Did you mean -D?'
: '';
ui.log('Invalid directory provided' + help, 'red', true);
process.exit(1);
}
debug('Directory specified, attempting to update');
const path = require('path');
const dir = path.resolve(argv.dir);
try {
if (this.ensureDir) {
const {ensureDirSync} = require('fs-extra');
ensureDirSync(dir);
}
process.chdir(dir);
} catch (error) {
/* istanbul ignore next */
const err = error.message || error.code || error;
ui.log(`Unable to use "${dir}" (error ${err}). Create the directory and try again.`, 'red', true);
process.exit(1);
}
debug('Finished updating directory');
}
if (!this.global) {
const findValidInstall = require('./utils/find-valid-install');
// NOTE: we disable recursive searching when the cwd is supplied
findValidInstall(commandName, !argv.dir);
}
if (!this.allowRoot && !argv.allowRoot) {
const checkRootUser = require('./utils/check-root-user');
// Check if user is trying to install as `root`
checkRootUser(commandName);
}
const system = new System(ui, extensions);
// Set the initial environment based on args or NODE_ENV
system.setEnvironment(argv.development || process.env.NODE_ENV === 'development', true);
// Instantiate the Command class
const commandInstance = new this(ui, system);
// Bind cleanup handler if one exists
if (commandInstance.cleanup) {
debug(`cleanup handler found for ${commandName}`);
const cleanup = commandInstance.cleanup.bind(commandInstance);
// bind cleanup handler to SIGINT, SIGTERM, and exit events
process.removeAllListeners('SIGINT').on('SIGINT', cleanup) // handle ctrl + c from keyboard
.removeAllListeners('SIGTERM').on('SIGTERM', cleanup) // handle kill signal from something like `kill`
.removeAllListeners('exit').on('exit', cleanup); // handle process.exit calls from within CLI codebase
}
try {
ui.log('');
ui.log('Love open source? We’re hiring JavaScript Engineers to work on Ghost full-time.', 'magentaBright');
ui.log('https://careers.ghost.org', 'magentaBright');
ui.log('');
debug('loading operating system information');
await ui.run(() => system.loadOsInfo(), 'Inspecting operating system', {clear: true});
if (!this.skipDeprecationCheck) {
debug('running deprecation checks');
const deprecationChecks = require('./utils/deprecation-checks');
await ui.run(() => deprecationChecks(ui, system), 'Checking for deprecations', {clear: true});
ui.log('');
}
if (this.runPreChecks) {
debug('running pre-checks');
const preChecks = require('./utils/pre-checks');
await preChecks(ui, system);
ui.log('');
}
debug(`running command ${commandName}`);
await commandInstance.run(argv);
} catch (error) {
debug(`command ${commandName} failed!`);
// Handle an error
ui.error(error, system);
process.exit(1);
}
}
/**
* Constructs the command instance
*
* @param {UI} ui UI instance
* @param {System} system System instance
*/
constructor(ui, system) {
this.ui = ui;
this.system = system;
}
/**
* @param {Object} argv Parsed arguments object
* @return Promise<void>|any
* @method run
* @public
*/
async run() {
throw new Error('Command must implement run function');
}
/**
* @param {Command} CommandClass Class of command to run
* @param {Object} argv Parsed arguments
* @return Promise<void>
* @method runCommand
* @public
*/
async runCommand(CommandClass, argv) {
if (!(CommandClass.prototype instanceof Command)) {
throw new Error('Provided command class does not extend the Command class');
}
const cmdInstance = new CommandClass(this.ui, this.system);
return cmdInstance.run(argv || {});
}
}
module.exports = Command;