text: /**
   * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
   * For licensing, see LICENSE.md.
   */
  
  'use strict';
  
  const chalk = require( 'chalk' );
  const path = require( 'path' );
  const fs = require( 'fs-extra' );
  
  module.exports = {
  	/**
  	 * Executes a shell command.
  	 *
  	 * @param {String} command The command to be executed.
  	 * @param {Object} options
  	 * @param {String} [options.verbosity='info'] Level of the verbosity. If set as 'info' both outputs (stdout and
  	 * stderr) will be logged. If set as 'error', only stderr output will be logged.
  	 * @returns {String} The command output.
  	 */
  	shExec( command, options = { verbosity: 'info' } ) {
  		const logger = require( './logger' );
  		const log = logger( options.verbosity );
  		const sh = require( 'shelljs' );
  
  		sh.config.silent = true;
  
  		const ret = sh.exec( command );
  
  		const grey = chalk.grey;
  
  		if ( ret.code ) {
  			if ( ret.stdout ) {
  				log.error( grey( ret.stdout ) );
  			}
  
  			if ( ret.stderr ) {
  				log.error( grey( ret.stderr ) );
  			}
  
  			throw new Error( `Error while executing ${ command }: ${ ret.stderr }` );
  		}
  
  		if ( ret.stdout ) {
  			log.info( grey( ret.stdout ) );
  		}
  
  		if ( ret.stderr ) {
  			log.info( grey( ret.stderr ) );
  		}
  
  		return ret.stdout;
  	},
  
  	/**
  	 * Links directory located in source path to directory located in destination path.
  	 * @param {String} source
  	 * @param {String} destination
  	 */
  	linkDirectories( source, destination ) {
  		const fs = require( 'fs' );
  		// Remove destination directory if exists.
  		if ( this.isSymlink( destination ) ) {
  			this.removeSymlink( destination );
  		} else if ( this.isDirectory( destination ) ) {
  			this.shExec( `rm -rf ${ destination }` );
  		}
  
  		fs.symlinkSync( source, destination, 'dir' );
  	},
  
  	/**
  	 * Returns array with all directories under specified path.
  	 *
  	 * @param {String} path
  	 * @returns {Array}
  	 */
  	getDirectories( path ) {
  		const fs = require( 'fs' );
  		const pth = require( 'path' );
  
  		return fs.readdirSync( path ).filter( item => {
  			return this.isDirectory( pth.join( path, item ) );
  		} );
  	},
  
  	/**
  	 * Returns true if path points to existing directory.
  	 *
  	 * @param {String} path
  	 * @returns {Boolean}
  	 */
  	isDirectory( path ) {
  		const fs = require( 'fs' );
  
  		try {
  			return fs.statSync( path ).isDirectory();
  		} catch ( e ) {
  			return false;
  		}
  	},
  
  	/**
  	 * Returns true if path points to existing file.
  	 *
  	 * @param {String} path
  	 * @returns {Boolean}
  	 */
  	isFile( path ) {
  		const fs = require( 'fs' );
  
  		try {
  			return fs.statSync( path ).isFile();
  		} catch ( e ) {
  			return false;
  		}
  	},
  
  	/**
  	 * Returns true if path points to symbolic link.
  	 *
  	 * @param {String} path
  	 */
  	isSymlink( path ) {
  		const fs = require( 'fs' );
  
  		try {
  			return fs.lstatSync( path ).isSymbolicLink();
  		} catch ( e ) {
  			return false;
  		}
  	},
  
  	/**
  	 * Updates JSON file under specified path.
  	 * @param {String} path Path to file on disk.
  	 * @param {Function} updateFunction Function that will be called with parsed JSON object. It should return
  	 * modified JSON object to save.
  	 */
  	updateJSONFile( path, updateFunction ) {
  		const fs = require( 'fs' );
  
  		const contents = fs.readFileSync( path, 'utf-8' );
  		let json = JSON.parse( contents );
  		json = updateFunction( json );
  
  		fs.writeFileSync( path, JSON.stringify( json, null, 2 ) + '\n', 'utf-8' );
  	},
  
  	/**
  	 * Reinserts all object's properties in alphabetical order (character's Unicode value).
  	 * Used for JSON.stringify method which takes keys in insertion order.
  	 *
  	 * @param { Object } obj
  	 * @returns { Object } Same object with sorted keys.
  	 */
  	sortObject( obj ) {
  		Object.keys( obj ).sort().forEach( key => {
  			const val = obj[ key ];
  			delete obj[ key ];
  			obj[ key ] = val;
  		} );
  
  		return obj;
  	},
  
  	/**
  	 * Returns name of the NPM module located under provided path.
  	 *
  	 * @param {String} modulePath Path to NPM module.
  	 */
  	readPackageName( modulePath ) {
  		const fs = require( 'fs' );
  		const path = require( 'path' );
  		const packageJSONPath = path.join( modulePath, 'package.json' );
  
  		if ( !this.isFile( packageJSONPath ) ) {
  			return null;
  		}
  
  		const contents = fs.readFileSync( packageJSONPath, 'utf-8' );
  		const json = JSON.parse( contents );
  
  		return json.name || null;
  	},
  
  	/**
  	 * Calls `npm install` command in specified path.
  	 *
  	 * @param {String} path
  	 */
  	npmInstall( path ) {
  		this.shExec( `cd ${ path } && npm install` );
  	},
  
  	/**
  	 * Calls `npm uninstall <name>` command in specified path.
  	 *
  	 * @param {String} path
  	 * @param {String} name
  	 */
  	npmUninstall( path, name ) {
  		this.shExec( `cd ${ path } && npm uninstall ${ name }` );
  	},
  
  	/**
  	 * Calls `npm update --dev` command in specified path.
  	 *
  	 * @param {String} path
  	 */
  	npmUpdate( path ) {
  		this.shExec( `cd ${ path } && npm update --dev` );
  	},
  
  	/**
  	 *
  	 * Copies file located under `inputPath` to `outputPath`. Optionally replaces contents of the file using provided
  	 * `replace` object.
  	 *
  	 *		// Each occurrence of `{{appName}}` inside README.md will be changed to `ckeditor5`.
  	 *		tools.copyTemplateFile( '/path/to/README.md', '/new/path/to/README.md', { '{{AppName}}': 'ckeditor5' } );
  	 *
  	 * @param {String} inputPath Path to input file.
  	 * @param {String} outputPath Path to output file.
  	 * @param {Object} [replace] Object with data to fill template. Method will take object's keys and replace their
  	 * occurrences with value stored under that key.
  	 */
  	copyTemplateFile( inputPath, outputPath, replace ) {
  		const fs = require( 'fs-extra' );
  		const path = require( 'path' );
  
  		// Create destination directory if one not exists.
  		fs.ensureDirSync( path.dirname( outputPath ) );
  
  		// If there is nothing to modify - just copy the file.
  		if ( !replace ) {
  			fs.copySync( inputPath, outputPath );
  
  			return;
  		}
  
  		// When replace object is passed - modify file on the fly.
  		const regexpList = [];
  
  		for ( const variableName in replace ) {
  			regexpList.push( variableName );
  		}
  
  		// Create one regexp for all variables to replace.
  		const regexp = new RegExp( regexpList.join( '|' ), 'g' );
  
  		// Read and modify.
  		const inputData = fs.readFileSync( inputPath, 'utf8' );
  		const modifiedData = inputData.replace( regexp, matched => replace[ matched ] );
  
  		// Save.
  		fs.writeFileSync( outputPath, modifiedData, 'utf8' );
  	},
  
  	/**
  	 * Copies a file. Takes care of creating parent directory if it doesn't exist.
  	 *
  	 * @param {String} from Source path.
  	 * @param {String} to Destination path (directory with file name).
  	 * @returns {Promise}
  	 */
  	copyFile( from, to ) {
  		return new Promise( ( resolve, reject ) => {
  			fs.readFile( from, 'utf8', ( err, data ) => {
  				if ( err ) {
  					return reject( err );
  				}
  
  				fs.outputFile( to, data, err => {
  					if ( err ) {
  						return reject( err );
  					}
  
  					return resolve();
  				} );
  			} );
  		} );
  	},
  
  	/**
  	 * Executes 'npm view' command for provided module name and returns Git url if one is found. Returns null if
  	 * module cannot be found.
  	 *
  	 * @param {String} name Name of the module.
  	 * @returns {*}
  	 */
  	getGitUrlFromNpm( name ) {
  		try {
  			const info = JSON.parse( this.shExec( `npm view ${ name } repository --json`, false ) );
  
  			if ( info && info.type == 'git' ) {
  				return info.url;
  			}
  		} catch ( error ) {
  			// Throw error only when different than E404.
  			if ( error.message.indexOf( 'npm ERR! code E404' ) == -1 ) {
  				throw error;
  			}
  		}
  
  		return null;
  	},
  
  	/**
  	 * Unlinks symbolic link under specified path.
  	 *
  	 * @param {String} path
  	 */
  	removeSymlink( path ) {
  		const fs = require( 'fs' );
  		fs.unlinkSync( path );
  	},
  
  	/**
  	 * Removes files and directories specified by `glob` starting from `rootDir`
  	 * and gently informs about deletion.
  	 *
  	 * @param {String} rootDir The path to the root directory (i.e. "dist/").
  	 * @param {String} glob Glob specifying what to clean.
  	 * @param {Object} options
  	 * @param {String} options.verbosity='info' A level of the verbosity for logger.
  	 * @returns {Promise}
  	 */
  	clean( rootDir, glob, options = { verbosity: 'info' } ) {
  		const del = require( 'del' );
  
  		return del( path.join( rootDir, glob ) )
  			.then( paths => {
  				const log = require( './logger' )( options.verbosity );
  
  				paths.forEach( p => {
  					log.info( `Deleted file '${ chalk.cyan( p ) }'.` );
  				} );
  			} );
  	}
  };
  