1 | ;
|
2 | /*
|
3 | * @adonisjs/ace
|
4 | *
|
5 | * (c) Harminder Virk <virk@adonisjs.com>
|
6 | *
|
7 | * For the full copyright and license information, please view the LICENSE
|
8 | * file that was distributed with this source code.
|
9 | */
|
10 | Object.defineProperty(exports, "__esModule", { value: true });
|
11 | exports.Kernel = void 0;
|
12 | const Hooks_1 = require("../Hooks");
|
13 | const Parser_1 = require("../Parser");
|
14 | const HelpCommand_1 = require("../HelpCommand");
|
15 | const Exceptions_1 = require("../Exceptions");
|
16 | const help_1 = require("../utils/help");
|
17 | const validateCommand_1 = require("../utils/validateCommand");
|
18 | const cliui_1 = require("@poppinss/cliui");
|
19 | /**
|
20 | * Ace kernel class is used to register, find and invoke commands by
|
21 | * parsing `process.argv.splice(2)` value.
|
22 | */
|
23 | class Kernel {
|
24 | constructor(application) {
|
25 | this.application = application;
|
26 | /**
|
27 | * Reference to hooks class to execute lifecycle
|
28 | * hooks
|
29 | */
|
30 | this.hooks = new Hooks_1.Hooks();
|
31 | /**
|
32 | * The state of the kernel
|
33 | */
|
34 | this.state = 'idle';
|
35 | /**
|
36 | * Exit handler for gracefully exiting the process
|
37 | */
|
38 | this.exitHandler = (kernel) => {
|
39 | if (kernel.error && typeof kernel.error.handle === 'function') {
|
40 | kernel.error.handle(kernel.error);
|
41 | }
|
42 | else if (kernel.error) {
|
43 | cliui_1.logger.fatal(kernel.error);
|
44 | }
|
45 | process.exit(kernel.exitCode === undefined ? 0 : kernel.exitCode);
|
46 | };
|
47 | /**
|
48 | * Find if CLI process is interactive. This flag can be
|
49 | * toggled programmatically
|
50 | */
|
51 | this.isInteractive = cliui_1.isInteractive;
|
52 | /**
|
53 | * Find if console output is mocked
|
54 | */
|
55 | this.isMockingConsoleOutput = false;
|
56 | /**
|
57 | * The default command that will be invoked when no command is
|
58 | * defined
|
59 | */
|
60 | this.defaultCommand = HelpCommand_1.HelpCommand;
|
61 | /**
|
62 | * List of registered commands
|
63 | */
|
64 | this.commands = {};
|
65 | this.aliases = this.application.rcFile.commandsAliases;
|
66 | /**
|
67 | * List of registered flags
|
68 | */
|
69 | this.flags = {};
|
70 | }
|
71 | /**
|
72 | * Executing global flag handlers. The global flag handlers are
|
73 | * not async as of now, but later we can look into making them
|
74 | * async.
|
75 | */
|
76 | executeGlobalFlagsHandlers(argv, command) {
|
77 | const globalFlags = Object.keys(this.flags);
|
78 | const parsedOptions = new Parser_1.Parser(this.flags).parse(argv, command);
|
79 | globalFlags.forEach((name) => {
|
80 | const value = parsedOptions[name];
|
81 | /**
|
82 | * Flag was not specified
|
83 | */
|
84 | if (value === undefined) {
|
85 | return;
|
86 | }
|
87 | /**
|
88 | * Calling the handler
|
89 | */
|
90 | this.flags[name].handler(parsedOptions[name], parsedOptions, command);
|
91 | });
|
92 | }
|
93 | /**
|
94 | * Returns an array of all registered commands
|
95 | */
|
96 | getAllCommandsAndAliases() {
|
97 | let commands = Object.keys(this.commands).map((name) => this.commands[name]);
|
98 | let aliases = {};
|
99 | /**
|
100 | * Concat manifest commands when they exists
|
101 | */
|
102 | if (this.manifestLoader && this.manifestLoader.booted) {
|
103 | const { commands: manifestCommands, aliases: manifestAliases } = this.manifestLoader.getCommands();
|
104 | commands = commands.concat(manifestCommands);
|
105 | aliases = Object.assign(aliases, manifestAliases);
|
106 | }
|
107 | return {
|
108 | commands,
|
109 | aliases: Object.assign(aliases, this.aliases),
|
110 | };
|
111 | }
|
112 | /**
|
113 | * Processes the args and sets values on the command instance
|
114 | */
|
115 | async processCommandArgsAndFlags(commandInstance, args) {
|
116 | const parser = new Parser_1.Parser(this.flags);
|
117 | const command = commandInstance.constructor;
|
118 | /**
|
119 | * Parse the command arguments. The `parse` method will raise exception if flag
|
120 | * or arg is not
|
121 | */
|
122 | const parsedOptions = parser.parse(args, command);
|
123 | /**
|
124 | * We validate the command arguments after the global flags have been
|
125 | * executed. It is required, since flags may have nothing to do
|
126 | * with the validaty of command itself
|
127 | */
|
128 | command.args.forEach((arg, index) => {
|
129 | parser.validateArg(arg, index, parsedOptions, command);
|
130 | });
|
131 | /**
|
132 | * Creating a new command instance and setting
|
133 | * parsed options on it.
|
134 | */
|
135 | commandInstance.parsed = parsedOptions;
|
136 | /**
|
137 | * Setup command instance argument and flag
|
138 | * properties.
|
139 | */
|
140 | for (let i = 0; i < command.args.length; i++) {
|
141 | const arg = command.args[i];
|
142 | if (arg.type === 'spread') {
|
143 | commandInstance[arg.propertyName] = parsedOptions._.slice(i);
|
144 | break;
|
145 | }
|
146 | else {
|
147 | commandInstance[arg.propertyName] = parsedOptions._[i];
|
148 | }
|
149 | }
|
150 | /**
|
151 | * Set flag value on the command instance
|
152 | */
|
153 | for (let flag of command.flags) {
|
154 | const flagValue = parsedOptions[flag.name];
|
155 | if (flagValue !== undefined) {
|
156 | commandInstance[flag.propertyName] = flagValue;
|
157 | }
|
158 | }
|
159 | }
|
160 | /**
|
161 | * Execute the main command. For calling commands within commands,
|
162 | * one must call "kernel.exec".
|
163 | */
|
164 | async execMain(commandName, args) {
|
165 | const command = await this.find([commandName]);
|
166 | /**
|
167 | * Command not found. So execute global flags handlers and
|
168 | * raise an exception
|
169 | */
|
170 | if (!command) {
|
171 | this.executeGlobalFlagsHandlers(args);
|
172 | throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName));
|
173 | }
|
174 | /**
|
175 | * Make an instance of the command
|
176 | */
|
177 | const commandInstance = await this.application.container.makeAsync(command, [
|
178 | this.application,
|
179 | this,
|
180 | ]);
|
181 | /**
|
182 | * Execute global flags
|
183 | */
|
184 | this.executeGlobalFlagsHandlers(args, command);
|
185 | /**
|
186 | * Process the arguments and flags for the command
|
187 | */
|
188 | await this.processCommandArgsAndFlags(commandInstance, args);
|
189 | /**
|
190 | * Keep a reference to the entry command. So that we know if we
|
191 | * want to entertain `.exit` or not
|
192 | */
|
193 | this.entryCommand = commandInstance;
|
194 | /**
|
195 | * Execute before run hooks
|
196 | */
|
197 | await this.hooks.execute('before', 'run', commandInstance);
|
198 | /**
|
199 | * Execute command
|
200 | */
|
201 | return commandInstance.exec();
|
202 | }
|
203 | /**
|
204 | * Handles exiting the process
|
205 | */
|
206 | async exitProcess(error) {
|
207 | /**
|
208 | * Check for state to avoid exiting the process multiple times
|
209 | */
|
210 | if (this.state === 'completed') {
|
211 | return;
|
212 | }
|
213 | this.state = 'completed';
|
214 | /**
|
215 | * Re-assign error if entry command exists and has error
|
216 | */
|
217 | if (!error && this.entryCommand && this.entryCommand.error) {
|
218 | error = this.entryCommand.error;
|
219 | }
|
220 | /**
|
221 | * Execute the after run hooks. Wrapping inside try/catch since this is the
|
222 | * cleanup handler for the process and must handle all exceptions
|
223 | */
|
224 | try {
|
225 | if (this.entryCommand) {
|
226 | await this.hooks.execute('after', 'run', this.entryCommand);
|
227 | }
|
228 | }
|
229 | catch (hookError) {
|
230 | error = hookError;
|
231 | }
|
232 | /**
|
233 | * Assign error to the kernel instance
|
234 | */
|
235 | if (error) {
|
236 | this.error = error;
|
237 | }
|
238 | /**
|
239 | * Figure out the exit code for the process
|
240 | */
|
241 | const exitCode = error ? 1 : 0;
|
242 | const commandExitCode = this.entryCommand && this.entryCommand.exitCode;
|
243 | this.exitCode = commandExitCode === undefined ? exitCode : commandExitCode;
|
244 | try {
|
245 | await this.exitHandler(this);
|
246 | }
|
247 | catch (exitHandlerError) {
|
248 | cliui_1.logger.warning('Expected exit handler to exit the process. Instead it raised an exception');
|
249 | cliui_1.logger.fatal(exitHandlerError);
|
250 | }
|
251 | }
|
252 | before(action, callback) {
|
253 | this.hooks.add('before', action, callback);
|
254 | return this;
|
255 | }
|
256 | after(action, callback) {
|
257 | this.hooks.add('after', action, callback);
|
258 | return this;
|
259 | }
|
260 | /**
|
261 | * Register an array of command constructors
|
262 | */
|
263 | register(commands) {
|
264 | commands.forEach((command) => {
|
265 | command.boot();
|
266 | (0, validateCommand_1.validateCommand)(command);
|
267 | this.commands[command.commandName] = command;
|
268 | /**
|
269 | * Registering command aliaes
|
270 | */
|
271 | command.aliases.forEach((alias) => (this.aliases[alias] = command.commandName));
|
272 | });
|
273 | return this;
|
274 | }
|
275 | /**
|
276 | * Register a global flag. It can be defined in combination with
|
277 | * any command.
|
278 | */
|
279 | flag(name, handler, options) {
|
280 | this.flags[name] = Object.assign({
|
281 | name,
|
282 | propertyName: name,
|
283 | handler,
|
284 | type: 'boolean',
|
285 | }, options);
|
286 | return this;
|
287 | }
|
288 | /**
|
289 | * Use manifest instance to lazy load commands
|
290 | */
|
291 | useManifest(manifestLoader) {
|
292 | this.manifestLoader = manifestLoader;
|
293 | return this;
|
294 | }
|
295 | /**
|
296 | * Register an exit handler
|
297 | */
|
298 | onExit(callback) {
|
299 | this.exitHandler = callback;
|
300 | return this;
|
301 | }
|
302 | /**
|
303 | * Returns an array of command names suggestions for a given name.
|
304 | */
|
305 | getSuggestions(name, distance = 3) {
|
306 | const leven = require('leven');
|
307 | const { commands, aliases } = this.getAllCommandsAndAliases();
|
308 | const suggestions = commands
|
309 | .filter(({ commandName }) => leven(name, commandName) <= distance)
|
310 | .map(({ commandName }) => commandName);
|
311 | return suggestions.concat(Object.keys(aliases).filter((alias) => leven(name, alias) <= distance));
|
312 | }
|
313 | /**
|
314 | * Preload the manifest file. Re-running this method twice will
|
315 | * result in a noop
|
316 | */
|
317 | async preloadManifest() {
|
318 | /**
|
319 | * Load manifest commands when instance of manifest loader exists.
|
320 | */
|
321 | if (this.manifestLoader) {
|
322 | await this.manifestLoader.boot();
|
323 | }
|
324 | }
|
325 | /**
|
326 | * Finds the command from the command line argv array. If command for
|
327 | * the given name doesn't exists, then it will return `null`.
|
328 | *
|
329 | * Does executes the before and after hooks regardless of whether the
|
330 | * command has been found or not
|
331 | */
|
332 | async find(argv) {
|
333 | /**
|
334 | * ----------------------------------------------------------------------------
|
335 | * Even though in `Unix` the command name may appear in between or at last, with
|
336 | * ace we always want the command name to be the first argument. However, the
|
337 | * arguments to the command itself can appear in any sequence. For example:
|
338 | *
|
339 | * Works
|
340 | * - node ace make:controller foo
|
341 | * - node ace make:controller --http foo
|
342 | *
|
343 | * Doesn't work
|
344 | * - node ace foo make:controller
|
345 | * ----------------------------------------------------------------------------
|
346 | */
|
347 | const [commandName] = argv;
|
348 | /**
|
349 | * Command name from the registered aliases
|
350 | */
|
351 | const aliasCommandName = this.aliases[commandName];
|
352 | /**
|
353 | * Manifest commands gets preference over manually registered commands.
|
354 | *
|
355 | * - We check the manifest loader is register
|
356 | * - The manifest loader has the command
|
357 | * - Or the manifest loader has the alias command
|
358 | */
|
359 | const commandNode = this.manifestLoader
|
360 | ? this.manifestLoader.hasCommand(commandName)
|
361 | ? this.manifestLoader.getCommand(commandName)
|
362 | : this.manifestLoader.hasCommand(aliasCommandName)
|
363 | ? this.manifestLoader.getCommand(aliasCommandName)
|
364 | : undefined
|
365 | : undefined;
|
366 | if (commandNode) {
|
367 | commandNode.command.aliases = commandNode.command.aliases || [];
|
368 | if (aliasCommandName && !commandNode.command.aliases.includes(commandName)) {
|
369 | commandNode.command.aliases.push(commandName);
|
370 | }
|
371 | await this.hooks.execute('before', 'find', commandNode.command);
|
372 | const command = await this.manifestLoader.loadCommand(commandNode.command.commandName);
|
373 | await this.hooks.execute('after', 'find', command);
|
374 | return command;
|
375 | }
|
376 | else {
|
377 | /**
|
378 | * Try to find command inside manually registered command or fallback
|
379 | * to null
|
380 | */
|
381 | const command = this.commands[commandName] || this.commands[aliasCommandName] || null;
|
382 | /**
|
383 | * Share main command name as an alias with the command
|
384 | */
|
385 | if (command) {
|
386 | command.aliases = command.aliases || [];
|
387 | if (aliasCommandName && !command.aliases.includes(commandName)) {
|
388 | command.aliases.push(commandName);
|
389 | }
|
390 | }
|
391 | /**
|
392 | * Executing before and after together to be compatible
|
393 | * with the manifest find before and after hooks
|
394 | */
|
395 | await this.hooks.execute('before', 'find', command);
|
396 | await this.hooks.execute('after', 'find', command);
|
397 | return command;
|
398 | }
|
399 | }
|
400 | /**
|
401 | * Run the default command. The default command doesn't accept
|
402 | * and args or flags.
|
403 | */
|
404 | async runDefaultCommand() {
|
405 | this.defaultCommand.boot();
|
406 | (0, validateCommand_1.validateCommand)(this.defaultCommand);
|
407 | /**
|
408 | * Execute before/after find hooks
|
409 | */
|
410 | await this.hooks.execute('before', 'find', this.defaultCommand);
|
411 | await this.hooks.execute('after', 'find', this.defaultCommand);
|
412 | /**
|
413 | * Make the command instance using the container
|
414 | */
|
415 | const commandInstance = await this.application.container.makeAsync(this.defaultCommand, [
|
416 | this.application,
|
417 | this,
|
418 | ]);
|
419 | /**
|
420 | * Execute before run hook
|
421 | */
|
422 | await this.hooks.execute('before', 'run', commandInstance);
|
423 | /**
|
424 | * Keep a reference to the entry command
|
425 | */
|
426 | this.entryCommand = commandInstance;
|
427 | /**
|
428 | * Execute the command
|
429 | */
|
430 | return commandInstance.exec();
|
431 | }
|
432 | /**
|
433 | * Find if a command is the main command. Main commands are executed
|
434 | * directly from the terminal.
|
435 | */
|
436 | isMain(command) {
|
437 | return !!this.entryCommand && this.entryCommand === command;
|
438 | }
|
439 | /**
|
440 | * Enforce mocking the console output. Command logs, tables, prompts
|
441 | * will be mocked
|
442 | */
|
443 | mockConsoleOutput() {
|
444 | this.isMockingConsoleOutput = true;
|
445 | return this;
|
446 | }
|
447 | /**
|
448 | * Toggle interactive state
|
449 | */
|
450 | interactive(state) {
|
451 | this.isInteractive = state;
|
452 | return this;
|
453 | }
|
454 | /**
|
455 | * Execute a command as a sub-command. Do not call "handle" and
|
456 | * always use this method to invoke command programatically
|
457 | */
|
458 | async exec(commandName, args) {
|
459 | const command = await this.find([commandName]);
|
460 | /**
|
461 | * Command not found.
|
462 | */
|
463 | if (!command) {
|
464 | throw Exceptions_1.InvalidCommandException.invoke(commandName, this.getSuggestions(commandName));
|
465 | }
|
466 | /**
|
467 | * Make an instance of command and keep a reference of it as `this.entryCommand`
|
468 | */
|
469 | const commandInstance = await this.application.container.makeAsync(command, [
|
470 | this.application,
|
471 | this,
|
472 | ]);
|
473 | /**
|
474 | * Process args and flags for the command
|
475 | */
|
476 | await this.processCommandArgsAndFlags(commandInstance, args);
|
477 | let commandError;
|
478 | /**
|
479 | * Wrapping the command execution inside a try/catch, so that
|
480 | * we can run the after hooks regardless of success or
|
481 | * failure
|
482 | */
|
483 | try {
|
484 | await this.hooks.execute('before', 'run', commandInstance);
|
485 | await commandInstance.exec();
|
486 | }
|
487 | catch (error) {
|
488 | commandError = error;
|
489 | }
|
490 | /**
|
491 | * Execute after hooks
|
492 | */
|
493 | await this.hooks.execute('after', 'run', commandInstance);
|
494 | /**
|
495 | * Re-throw error (if any)
|
496 | */
|
497 | if (commandError) {
|
498 | throw commandError;
|
499 | }
|
500 | return commandInstance;
|
501 | }
|
502 | /**
|
503 | * Makes instance of a given command by processing command line arguments
|
504 | * and setting them on the command instance
|
505 | */
|
506 | async handle(argv) {
|
507 | if (this.state !== 'idle') {
|
508 | return;
|
509 | }
|
510 | this.state = 'running';
|
511 | try {
|
512 | /**
|
513 | * Preload the manifest file to load the manifest files
|
514 | */
|
515 | this.preloadManifest();
|
516 | /**
|
517 | * Branch 1
|
518 | * Run default command and invoke the exit handler
|
519 | */
|
520 | if (!argv.length) {
|
521 | await this.runDefaultCommand();
|
522 | await this.exitProcess();
|
523 | return;
|
524 | }
|
525 | /**
|
526 | * Branch 2
|
527 | * No command has been mentioned and hence execute all the global flags
|
528 | * invoke the exit handler
|
529 | */
|
530 | const hasMentionedCommand = !argv[0].startsWith('-');
|
531 | if (!hasMentionedCommand) {
|
532 | this.executeGlobalFlagsHandlers(argv);
|
533 | await this.exitProcess();
|
534 | return;
|
535 | }
|
536 | /**
|
537 | * Branch 3
|
538 | * Execute the given command as the main command
|
539 | */
|
540 | const [commandName, ...args] = argv;
|
541 | await this.execMain(commandName, args);
|
542 | /**
|
543 | * Exit the process if there isn't any entry command
|
544 | */
|
545 | if (!this.entryCommand) {
|
546 | await this.exitProcess();
|
547 | return;
|
548 | }
|
549 | const entryCommandConstructor = this.entryCommand.constructor;
|
550 | /**
|
551 | * Exit the process if entry command isn't a stayalive command. Stayalive
|
552 | * commands should call `this.exit` to exit the process.
|
553 | */
|
554 | if (!entryCommandConstructor.settings.stayAlive) {
|
555 | await this.exitProcess();
|
556 | }
|
557 | }
|
558 | catch (error) {
|
559 | await this.exitProcess(error);
|
560 | }
|
561 | }
|
562 | /**
|
563 | * Print the help screen for a given command or all commands/flags
|
564 | */
|
565 | printHelp(command, commandsToAppend, aliasesToAppend) {
|
566 | let { commands, aliases } = this.getAllCommandsAndAliases();
|
567 | /**
|
568 | * Append additional commands and aliases for help screen only
|
569 | */
|
570 | if (commandsToAppend) {
|
571 | commands = commands.concat(commandsToAppend);
|
572 | }
|
573 | if (aliasesToAppend) {
|
574 | aliases = Object.assign({}, aliases, aliasesToAppend);
|
575 | }
|
576 | if (command) {
|
577 | (0, help_1.printHelpFor)(command, aliases);
|
578 | }
|
579 | else {
|
580 | const flags = Object.keys(this.flags).map((name) => this.flags[name]);
|
581 | (0, help_1.printHelp)(commands, flags, aliases);
|
582 | }
|
583 | }
|
584 | /**
|
585 | * Trigger kernel to exit the process. The call to this method
|
586 | * is ignored when command is not same the `entryCommand`.
|
587 | *
|
588 | * In other words, subcommands cannot trigger exit
|
589 | */
|
590 | async exit(command, error) {
|
591 | if (command !== this.entryCommand) {
|
592 | return;
|
593 | }
|
594 | await this.exitProcess(error);
|
595 | }
|
596 | }
|
597 | exports.Kernel = Kernel;
|