瀏覽代碼

Merge branch 't/36-dev-init' into t/36

Szymon Kupś 10 年之前
父節點
當前提交
1f2bdb4a2e

+ 54 - 31
dev/tasks/dev.js

@@ -1,36 +1,59 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
 'use strict';
 'use strict';
 
 
-var tools = require( './utils/tools' );
-var path = require( 'path' );
-var ckeditor5Path = process.cwd();
-var json = require( path.join( ckeditor5Path, 'package.json' ) );
-var dependencies = json.dependencies;
-
-module.exports = function( grunt ) {
-	grunt.registerTask( 'dev', function( target ) {
-		var pluginPath;
-
-		switch ( target ) {
-			case 'init':
-				var ckeDependencies = tools.getCKEditorDependencies( dependencies );
-				var regexp = /^ckeditor\//;
-				var location = path.join( ckeditor5Path, '..' );
-
-				if ( ckeDependencies ) {
-					Object.keys( ckeDependencies ).forEach( function( name ) {
-						// Check if CKEditor GitHub url.
-						if ( regexp.test( ckeDependencies[ name ] ) ) {
-							grunt.log.writeln( 'Clonning repository ' + ckeDependencies[ name ] + '...' );
-							tools.cloneRepository( ckeDependencies[ name ], location );
-
-							pluginPath = path.join( location, name );
-							grunt.log.writeln( 'Linking ' + pluginPath + ' into ' + ckeditor5Path + '...' );
-							tools.npmLink( pluginPath, ckeditor5Path, name );
-						}
-					} );
-				}
-				break;
-		}
+const initTask = require( './utils/dev-init' );
+const pluginCreateTask = require( './utils/dev-plugin-create' );
+const pluginInstallTask = require( './utils/dev-plugin-install' );
+const pluginUpdateTask = require( './utils/dev-update' );
+const pluginStatusTask = require( './utils/dev-status' );
+const boilerplateUpdateTask = require( './utils/dev-boilerplate-update' );
+const ckeditor5Path = process.cwd();
+
+module.exports = ( grunt ) => {
+	const packageJSON = grunt.config.data.pkg;
+
+	grunt.registerTask( 'dev-init', function() {
+		const options = getOptions( this );
+		initTask( ckeditor5Path, packageJSON, options, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-plugin-create', function() {
+		const done = this.async();
+		const options = getOptions( this );
+		pluginCreateTask( ckeditor5Path, options, grunt.log.writeln, grunt.log.error ).then( done );
+	} );
+
+	grunt.registerTask( 'dev-plugin-install', function() {
+		const done = this.async();
+		const options = getOptions( this );
+		pluginInstallTask( ckeditor5Path, options, grunt.log.writeln, grunt.log.error ).then( done );
 	} );
 	} );
+
+	grunt.registerTask( 'dev-update', function() {
+		const options = getOptions( this );
+		pluginUpdateTask( ckeditor5Path, packageJSON, options, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-status', function() {
+		const options = getOptions( this );
+		pluginStatusTask( ckeditor5Path, packageJSON, options, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-boilerplate-update', function() {
+		const options = getOptions( this );
+		boilerplateUpdateTask( ckeditor5Path, packageJSON, options, grunt.log.writeln, grunt.log.error );
+	} );
+
+	function getOptions( context ) {
+		const options = {
+			workspaceRoot: '..'
+		};
+
+		return context.options( options );
+	}
 };
 };
 
 

+ 119 - 0
dev/tasks/tests/dev-init.js

@@ -0,0 +1,119 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+/* global describe, it, beforeEach, afterEach */
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const tools = require( '../utils/tools' );
+const git = require( '../utils/git' );
+const path = require( 'path' );
+const emptyFn = () => { };
+let spies;
+
+describe( 'dev-tasks', () => {
+	describe( 'dev-init', () => {
+		const initTask = require( '../utils/dev-init' );
+		const mainRepositoryPath = '/path/to/repository';
+		const options = {
+			workspaceRoot: '..'
+		};
+		const workspacePath = path.join( mainRepositoryPath, options.workspaceRoot );
+
+		beforeEach( () => createSpies() );
+		afterEach( () => restoreSpies() );
+
+		it( 'task should exists', () => expect( initTask ).to.be.a( 'function' ) );
+
+		it( 'performs no action when no ckeditor dependencies are found', () => {
+			const packageJSON = {
+				dependencies: {
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			initTask( mainRepositoryPath, packageJSON, options, emptyFn, emptyFn );
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.getDirectories.called ).to.equal( false );
+			expect( spies.parseRepositoryUrl.called ).to.equal( false );
+			expect( spies.cloneRepository.called ).to.equal( false );
+			expect( spies.checkout.called ).to.equal( false );
+			expect( spies.npmInstall.called ).to.equal( false );
+			expect( spies.installGitHooks.called ).to.equal( false );
+		} );
+
+		it( 'clones repositories if no directories are found', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			initTask( mainRepositoryPath, packageJSON, options, emptyFn, emptyFn );
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.getDirectories.calledOnce ).to.equal( true );
+			expect( spies.getDirectories.firstCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, options.workspaceRoot ) );
+			expect( spies.parseRepositoryUrl.calledTwice ).to.equal( true );
+			expect( spies.cloneRepository.calledTwice ).to.equal( true );
+			expect( spies.cloneRepository.firstCall.args[ 0 ] ).to.equal( spies.parseRepositoryUrl.firstCall.returnValue );
+			expect( spies.cloneRepository.firstCall.args[ 1 ] ).to.equal( workspacePath );
+			expect( spies.cloneRepository.secondCall.args[ 0 ] ).to.equal( spies.parseRepositoryUrl.secondCall.returnValue );
+			expect( spies.cloneRepository.secondCall.args[ 1 ] ).to.equal( workspacePath );
+			expect( spies.checkout.calledTwice ).to.equal( true );
+			expect( spies.linkDirectories.calledTwice ).to.equal( true );
+			expect( spies.npmInstall.calledTwice ).to.equal( true );
+			expect( spies.installGitHooks.calledTwice ).to.equal( true );
+		} );
+
+		it( 'only checks out repositories if directories are found', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			spies.getDirectories.restore();
+			spies.getDirectories = sinon.stub( tools, 'getCKE5Directories', () => [ 'ckeditor5-core', 'ckeditor5-plugin-devtest' ] );
+
+			initTask( mainRepositoryPath, packageJSON, options, emptyFn, emptyFn );
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.getDirectories.calledOnce ).to.equal( true );
+			expect( spies.getDirectories.firstCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, options.workspaceRoot ) );
+			expect( spies.parseRepositoryUrl.calledTwice ).to.equal( true );
+			expect( spies.cloneRepository.called ).to.equal( false );
+			expect( spies.checkout.calledTwice ).to.equal( true );
+			expect( spies.linkDirectories.calledTwice ).to.equal( true );
+			expect( spies.npmInstall.calledTwice ).to.equal( true );
+			expect( spies.installGitHooks.calledTwice ).to.equal( true );
+		} );
+	} );
+
+	function createSpies() {
+		spies = {
+			getDependencies: sinon.spy( tools, 'getCKEditorDependencies' ),
+			getDirectories: sinon.stub( tools, 'getCKE5Directories', () => [] ),
+			parseRepositoryUrl: sinon.spy( git, 'parseRepositoryUrl' ),
+			cloneRepository: sinon.stub( git, 'cloneRepository' ),
+			linkDirectories: sinon.stub( tools, 'linkDirectories' ),
+			checkout: sinon.stub( git, 'checkout' ),
+			npmInstall: sinon.stub( tools, 'npmInstall' ),
+			installGitHooks: sinon.stub( tools, 'installGitHooks' )
+		};
+	}
+
+	function restoreSpies() {
+		for ( let spy in spies ) {
+			spies[ spy ].restore();
+		}
+	}
+} );

+ 199 - 0
dev/tasks/tests/git.js

@@ -0,0 +1,199 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+/* global describe, it, beforeEach, afterEach */
+
+let toRestore;
+const git = require( '../utils/git' );
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const tools = require( '../utils/tools' );
+const expect = chai.expect;
+
+describe( 'utils', () => {
+	beforeEach( () => toRestore = [] );
+
+	afterEach( () => {
+		toRestore.forEach( item => item.restore() );
+	} );
+
+	describe( 'git', () => {
+		describe( 'parseRepositoryUrl', () => {
+			it( 'should be defined', () => expect( git.parseRepositoryUrl ).to.be.a( 'function' ) );
+
+			it( 'should parse short GitHub URL ', () => {
+				const urlInfo = git.parseRepositoryUrl( 'ckeditor/ckeditor5-core' );
+
+				expect( urlInfo.server ).to.equal( 'https://github.com/' );
+				expect( urlInfo.branch ).to.equal( 'master' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse short GitHub URL with provided branch ', () => {
+				const urlInfo = git.parseRepositoryUrl( 'ckeditor/ckeditor5-core#experimental' );
+
+				expect( urlInfo.server ).to.equal( 'https://github.com/' );
+				expect( urlInfo.branch ).to.equal( 'experimental' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (http)', () => {
+				const urlInfo = git.parseRepositoryUrl( 'http://github.com/ckeditor/ckeditor5-core' );
+
+				expect( urlInfo.server ).to.equal( 'http://github.com/' );
+				expect( urlInfo.branch ).to.equal( 'master' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (http) with provided branch', () => {
+				const urlInfo = git.parseRepositoryUrl( 'http://github.com/ckeditor/ckeditor5-core#experimental' );
+
+				expect( urlInfo.server ).to.equal( 'http://github.com/' );
+				expect( urlInfo.branch ).to.equal( 'experimental' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (https)', () => {
+				const urlInfo = git.parseRepositoryUrl( 'https://github.com/ckeditor/ckeditor5-core' );
+
+				expect( urlInfo.server ).to.equal( 'https://github.com/' );
+				expect( urlInfo.branch ).to.equal( 'master' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (https) with provided branch', () => {
+				const urlInfo = git.parseRepositoryUrl( 'https://github.com/ckeditor/ckeditor5-core#t/122' );
+
+				expect( urlInfo.server ).to.equal( 'https://github.com/' );
+				expect( urlInfo.branch ).to.equal( 't/122' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (git)', () => {
+				const urlInfo = git.parseRepositoryUrl( 'git@github.com:ckeditor/ckeditor5-core' );
+
+				expect( urlInfo.server ).to.equal( 'git@github.com:' );
+				expect( urlInfo.branch ).to.equal( 'master' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+
+			it( 'should parse full GitHub URL (git) with provided branch', () => {
+				const urlInfo = git.parseRepositoryUrl( 'git@github.com:ckeditor/ckeditor5-core#new-feature' );
+
+				expect( urlInfo.server ).to.equal( 'git@github.com:' );
+				expect( urlInfo.branch ).to.equal( 'new-feature' );
+				expect( urlInfo.repository ).to.equal( 'ckeditor/ckeditor5-core' );
+			} );
+		} );
+
+		describe( 'cloneRepository', () => {
+			it( 'should be defined', () => expect( git.cloneRepository ).to.be.a( 'function' ) );
+
+			it( 'should call clone commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const workspacePath = '/path/to/workspace/';
+				const urlInfo = git.parseRepositoryUrl( 'git@github.com:ckeditor/ckeditor5-core#new-feature' );
+				const cloneCommands = `cd ${ workspacePath } && git clone ${ urlInfo.server + urlInfo.repository }`;
+				toRestore.push( shExecStub );
+
+				git.cloneRepository( urlInfo, workspacePath );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( cloneCommands );
+			} );
+		} );
+
+		describe( 'checkout', () => {
+			it( 'should be defined', () => expect( git.checkout ).to.be.a( 'function' ) );
+
+			it( 'should call checkout commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const branchName = 'branch-to-checkout';
+				const checkoutCommands = `cd ${ repositoryLocation } && git checkout ${ branchName }`;
+				toRestore.push( shExecStub );
+
+				git.checkout( repositoryLocation, branchName );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( checkoutCommands );
+			} );
+		} );
+
+		describe( 'pull', () => {
+			it( 'should be defined', () => expect( git.pull ).to.be.a( 'function' ) );
+			it( 'should call pull commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const branchName = 'branch-to-pull';
+				const pullCommands = `cd ${ repositoryLocation } && git pull origin ${ branchName }`;
+				toRestore.push( shExecStub );
+
+				git.pull( repositoryLocation, branchName );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( pullCommands );
+			} );
+		} );
+
+		describe( 'initializeRepository', () => {
+			it( 'should be defined', () => expect( git.initializeRepository ).to.be.a( 'function' ) );
+			it( 'should call initialize commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const initializeCommands = [
+					`git init ${ repositoryLocation }`,
+					`cd ${ repositoryLocation }`,
+					`git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`,
+					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
+					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
+				];
+				toRestore.push( shExecStub );
+
+				git.initializeRepository( repositoryLocation );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( initializeCommands.join( ' && ' ) );
+			} );
+		} );
+
+		describe( 'getStatus', () => {
+			it( 'should be defined', () => expect( git.getStatus ).to.be.a( 'function' ) );
+			it( 'should call status command', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const statusCommands = `cd ${ repositoryLocation } && git status --porcelain -sb`;
+				toRestore.push( shExecStub );
+
+				git.getStatus( repositoryLocation );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( statusCommands );
+			} );
+		} );
+
+		describe( 'updateBoilerplate', () => {
+			it( 'should be defined', () => expect( git.updateBoilerplate ).to.be.a( 'function' ) );
+			it( 'should call update boilerplate commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const addRemoteCommands = `cd ${ repositoryLocation } && git remote add boilerplate ${ git.BOILERPLATE_REPOSITORY }`;
+				const updateCommands = [
+					`cd ${ repositoryLocation }`,
+					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
+					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
+				];
+				toRestore.push( shExecStub );
+
+				git.updateBoilerplate( repositoryLocation );
+
+				expect( shExecStub.calledTwice ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( addRemoteCommands );
+				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( updateCommands.join( ' && ' ) );
+			} );
+		} );
+	} );
+} );

+ 166 - 0
dev/tasks/tests/tools.js

@@ -0,0 +1,166 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+/* global describe, it, beforeEach, afterEach */
+
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const tools = require( '../utils/tools' );
+const path = require( 'path' );
+let toRestore;
+
+describe( 'utils', () => {
+	beforeEach( () => toRestore = [] );
+
+	afterEach( () => {
+		toRestore.forEach( item => item.restore() );
+	} );
+
+	describe( 'tools', () => {
+		describe( 'linkDirectories', () => {
+			it( 'should be defined', () => expect( tools.linkDirectories ).to.be.a( 'function' ) );
+
+			it( 'should run link commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( false );
+				const source = '/source/dir';
+				const destination = '/destination/dir';
+				toRestore.push( shExecStub, isDirectoryStub );
+
+				tools.linkDirectories( source, destination );
+
+				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `ln -s ${ source } ${ destination }` );
+			} );
+
+			it( 'should remove destination directory before linking', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
+				const source = '/source/dir';
+				const destination = '/destination/dir';
+				toRestore.push( shExecStub, isDirectoryStub );
+
+				tools.linkDirectories( source, destination );
+
+				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( shExecStub.calledTwice ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `rm -rf ${ destination }` );
+				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( `ln -s ${ source } ${ destination }` );
+			} );
+		} );
+
+		describe( 'getCKEditorDependencies', () => {
+			it( 'should be defined', () => expect( tools.getCKEditorDependencies ).to.be.a( 'function' ) );
+
+			it( 'should return null if no CKEditor5 repository is found', () => {
+				const dependencies = {
+					'plugin1': '',
+					'plugin2': '',
+					'plugin3': ''
+				};
+				expect( tools.getCKEditorDependencies( dependencies ) ).to.equal( null );
+			} );
+
+			it( 'should return only ckeditor5- dependencies', () => {
+				const dependencies = {
+					'plugin1': '',
+					'ckeditor5-plugin-image': 'ckeditor/ckeditor5-plugin-image',
+					'plugin2': '',
+					'ckeditor5-core': 'ckeditor/ckeditor5-core'
+				};
+				const ckeditorDependencies = tools.getCKEditorDependencies( dependencies );
+
+				expect( ckeditorDependencies ).to.be.an( 'object' );
+				expect( ckeditorDependencies.plugin1 ).to.be.a( 'undefined' );
+				expect( ckeditorDependencies.plugin2 ).to.be.a( 'undefined' );
+				expect( ckeditorDependencies[ 'ckeditor5-plugin-image' ] ).to.be.a( 'string' );
+				expect( ckeditorDependencies[ 'ckeditor5-core' ] ).to.be.a( 'string' );
+			} );
+		} );
+
+		describe( 'getDirectories', () => {
+			it( 'should be defined', () => expect( tools.getDirectories ).to.be.a( 'function' ) );
+
+			it( 'should get directories in specified path', () => {
+				const fs = require( 'fs' );
+				const directories = [ 'dir1', 'dir2', 'dir3' ];
+				const readdirSyncStub = sinon.stub( fs, 'readdirSync', () => directories );
+				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
+				const dirPath = 'path';
+				toRestore.push( readdirSyncStub, isDirectoryStub );
+
+				tools.getDirectories( dirPath );
+
+				expect( readdirSyncStub.calledOnce ).to.equal( true );
+				expect( isDirectoryStub.calledThrice ).to.equal( true );
+				expect( isDirectoryStub.firstCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 0 ] ) );
+				expect( isDirectoryStub.secondCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 1 ] ) );
+				expect( isDirectoryStub.thirdCall.args[ 0 ] ).to.equal( path.join( dirPath, directories[ 2 ] ) );
+			} );
+		} );
+
+		describe( 'isDirectory', () => {
+			it( 'should be defined', () => expect( tools.isDirectory ).to.be.a( 'function' ) );
+
+			it( 'should return true if path points to directory', () => {
+				const fs = require( 'fs' );
+				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => true } ) );
+				const path = 'path';
+				toRestore.push( statSyncStub );
+
+				const result = tools.isDirectory( path );
+
+				expect( statSyncStub.calledOnce ).to.equal( true );
+				expect( statSyncStub.firstCall.args[ 0 ] ).to.equal( path );
+				expect( result ).to.equal( true );
+			} );
+
+			it( 'should return false if path does not point to directory', () => {
+				const fs = require( 'fs' );
+				const statSyncStub = sinon.stub( fs, 'statSync', () => ( { isDirectory: () => false } ) );
+				const path = 'path';
+				toRestore.push( statSyncStub );
+
+				const result = tools.isDirectory( path );
+
+				expect( statSyncStub.calledOnce ).to.equal( true );
+				expect( statSyncStub.firstCall.args[ 0 ] ).to.equal( path );
+				expect( result ).to.equal( false );
+			} );
+
+			it( 'should return false if statSync method throws', () => {
+				const fs = require( 'fs' );
+				const statSyncStub = sinon.stub( fs, 'statSync' ).throws();
+				const path = 'path';
+				toRestore.push( statSyncStub );
+
+				const result = tools.isDirectory( path );
+
+				expect( statSyncStub.calledOnce ).to.equal( true );
+				expect( statSyncStub.firstCall.args[ 0 ] ).to.equal( path );
+				expect( result ).to.equal( false );
+			} );
+		} );
+
+		describe( 'getCKE5Directories', () => {
+			it( 'should be defined', () => expect( tools.getCKE5Directories ).to.be.a( 'function' ) );
+
+			it( 'should return only ckeditor5 directories', () => {
+				const workspacePath = '/workspace/path';
+				const sourceDirectories = [ 'tools', 'ckeditor5', 'ckeditor5-core', '.bin', 'ckeditor5-plugin-image' ];
+				const getDirectoriesStub = sinon.stub( tools, 'getDirectories', () => sourceDirectories );
+				toRestore.push( getDirectoriesStub );
+				const directories = tools.getCKE5Directories( workspacePath );
+
+				expect( directories.length ).equal( 2 );
+				expect( directories[ 0 ] ).equal( 'ckeditor5-core' );
+				expect( directories[ 1 ] ).equal( 'ckeditor5-plugin-image' );
+			} );
+		} );
+	} );
+} );

+ 48 - 0
dev/tasks/utils/dev-boilerplate-update.js

@@ -0,0 +1,48 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const tools = require( './tools' );
+const git = require( './git' );
+const path = require( 'path' );
+
+/**
+ * 1. Get CKEditor5 dependencies from package.json file.
+ * 2. Scan workspace for repositories that match dependencies from package.json file.
+ * 3. Fetch and merge boilerplate remote.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, options, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+
+	// Get all CKEditor dependencies from package.json.
+	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+
+	if ( dependencies ) {
+		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+
+		for ( let dependency in dependencies ) {
+			const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
+
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( dependency ) > -1 ) {
+				try {
+					writeln( `Updating boilerplate in ${ dependency }...` );
+					git.updateBoilerplate( repositoryAbsolutePath );
+				} catch ( error ) {
+					writeError( error );
+				}
+			}
+		}
+	} else {
+		writeln( 'No CKEditor5 dependencies found in package.json file.' );
+	}
+};

+ 71 - 0
dev/tasks/utils/dev-init.js

@@ -0,0 +1,71 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const tools = require( './tools' );
+const git = require( './git' );
+const path = require( 'path' );
+
+/**
+ * 1. Get CKEditor5 dependencies from package.json file.
+ * 2. Check if any of the repositories are already present in the workspace.
+ * 		2.1. If repository is present in the workspace, check it out to desired branch if one is provided.
+ * 		2.2. If repository is not present in the workspace, clone it and checkout to desired branch if one is provided.
+ * 3. Link each new repository to node_modules. (do not use npm link, use standard linking instead)
+ * 4. Run `npm install` in each repository.
+ * 5. Install Git hooks in each repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, options, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+
+	// Get all CKEditor dependencies from package.json.
+	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+
+	if ( dependencies ) {
+		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+
+		for ( let dependency in dependencies ) {
+			const repositoryURL = dependencies[ dependency ];
+			const urlInfo = git.parseRepositoryUrl( repositoryURL );
+			const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
+
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( dependency ) === -1 ) {
+				try {
+					writeln( `Clonning ${ repositoryURL }...` );
+					git.cloneRepository( urlInfo, workspaceAbsolutePath );
+				} catch ( error ) {
+					writeError( error );
+				}
+			}
+
+			// Check out proper branch.
+			try {
+				writeln( `Checking out ${ repositoryURL } to ${ urlInfo.branch }...` );
+				git.checkout( repositoryAbsolutePath, urlInfo.branch );
+
+				writeln( `Linking ${ repositoryURL }...` );
+				tools.linkDirectories( repositoryAbsolutePath, path.join( ckeditor5Path, 'node_modules' , dependency ) );
+
+				writeln( `Running npm install in ${ repositoryURL }.` );
+				tools.npmInstall( repositoryAbsolutePath );
+
+				writeln( `Installing GIT hooks in ${ repositoryURL }.` );
+				tools.installGitHooks( repositoryAbsolutePath );
+			} catch ( error ) {
+				writeError( error );
+			}
+		}
+	} else {
+		writeln( 'No CKEditor5 dependencies found in package.json file.' );
+	}
+};

+ 90 - 0
dev/tasks/utils/dev-plugin-create.js

@@ -0,0 +1,90 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const inquiries = require( './inquiries' );
+const git = require( './git' );
+const tools = require( './tools' );
+const path = require( 'path' );
+
+/**
+ * 1. Ask for new plugin name.
+ * 2. Ask for initial version.
+ * 3. Ask for GitHub URL.
+ * 4. Initialize repository
+ * 		4.1. Initialize GIT repository.
+ * 		4.2. Fetch and merge boilerplate project.
+ * 5. Update package.json file in new plugin's repository.
+ * 6. Update package.json file in CKEditor5 repository.
+ * 7. Link new plugin.
+ * 8. Call `npm install` in plugin repository.
+ * 9. Install Git hooks in plugin repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ * @returns {Promise} Returns promise fulfilled after task is done.
+ */
+module.exports = ( ckeditor5Path, options, writeln, writeError ) => {
+	return new Promise( ( resolve, reject ) => {
+		const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+		let pluginName;
+		let repositoryPath;
+		let pluginVersion;
+		let gitHubUrl;
+
+		inquiries.getPluginName()
+			.then( result => {
+				pluginName = result;
+				repositoryPath = path.join( workspaceAbsolutePath, pluginName );
+
+				return inquiries.getPluginVersion();
+			} )
+			.then( result => {
+				pluginVersion = result;
+
+				return inquiries.getPluginGitHubUrl( pluginName );
+			} )
+			.then( result => {
+				try {
+					gitHubUrl = result;
+
+					writeln( `Initializing repository ${ repositoryPath }...` );
+					git.initializeRepository( repositoryPath );
+
+					writeln( `Updating package.json files...` );
+					tools.updateJSONFile( path.join( repositoryPath, 'package.json' ), ( json ) => {
+						json.name = pluginName;
+						json.version = pluginVersion;
+
+						return json;
+					} );
+
+					tools.updateJSONFile( path.join( ckeditor5Path, 'package.json' ), ( json ) => {
+						if ( !json.dependencies ) {
+							json.dependencies = {};
+						}
+						json.dependencies[ pluginName ] = gitHubUrl;
+
+						return json;
+					} );
+
+					writeln( `Linking ${ pluginName } to node_modules...` );
+					tools.linkDirectories( repositoryPath, path.join( ckeditor5Path, 'node_modules', pluginName ) );
+
+					writeln( `Running npm install in ${ pluginName }.` );
+					tools.npmInstall( repositoryPath );
+
+					writeln( `Installing GIT hooks in ${ pluginName }.` );
+					tools.installGitHooks( repositoryPath );
+				} catch ( error ) {
+					writeError( error );
+				}
+			} )
+			.catch( reject );
+	} );
+};

+ 78 - 0
dev/tasks/utils/dev-plugin-install.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const inquiries = require( './inquiries' );
+const git = require( './git' );
+const tools = require( './tools' );
+const path = require( 'path' );
+
+/**
+ * 1. Ask for plugin name.
+ * 2. Ask for GitHub URL.
+ * 3. Clone repository from provided GitHub URL.
+ * 4. Checkout repository to provided branch (or master if no branch is provided).
+ * 5. Update package.json file in CKEditor5 repository.
+ * 6. Link new plugin.
+ * 7. Call `npm install` in plugin repository.
+ * 8. Install Git hooks in plugin repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ * @returns {Promise} Returns promise fulfilled after task is done.
+ */
+module.exports = ( ckeditor5Path, options, writeln, writeError ) => {
+	return new Promise( ( resolve, reject ) => {
+		const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+		let pluginName;
+		let repositoryPath;
+		let gitHubUrl;
+
+		inquiries.getPluginName()
+			.then( result => {
+				pluginName = result;
+				repositoryPath = path.join( workspaceAbsolutePath, pluginName );
+
+				return inquiries.getPluginGitHubUrl( pluginName );
+			} )
+			.then( result => {
+				gitHubUrl = result;
+				let urlInfo = git.parseRepositoryUrl( gitHubUrl );
+
+				try {
+					writeln( `Clonning ${ gitHubUrl }...` );
+					git.cloneRepository( urlInfo, workspaceAbsolutePath );
+
+					writeln( `Checking out ${ gitHubUrl } to ${ urlInfo.branch }...` );
+					git.checkout( repositoryPath, urlInfo.branch );
+
+					writeln( `Updating package.json files...` );
+					tools.updateJSONFile( path.join( ckeditor5Path, 'package.json' ), ( json ) => {
+						if ( !json.dependencies ) {
+							json.dependencies = {};
+						}
+						json.dependencies[ pluginName ] = gitHubUrl;
+
+						return json;
+					} );
+
+					writeln( `Linking ${ pluginName } to node_modules...` );
+					tools.linkDirectories( repositoryPath, path.join( ckeditor5Path, 'node_modules', pluginName ) );
+
+					writeln( `Running npm install in ${ pluginName }.` );
+					tools.npmInstall( repositoryPath );
+
+					writeln( `Installing GIT hooks in ${ pluginName }.` );
+					tools.installGitHooks( repositoryPath );
+				} catch ( error ) {
+					writeError( error );
+				}
+			} )
+			.catch( reject );
+	} );
+};

+ 49 - 0
dev/tasks/utils/dev-status.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const tools = require( './tools' );
+const git = require( './git' );
+const path = require( 'path' );
+
+/**
+ * 1. Get CKEditor5 dependencies from package.json file.
+ * 2. Scan workspace for repositories that match dependencies from package.json file.
+ * 3. Print GIT status using `git status --porcelain -sb` command.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, options, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+
+	// Get all CKEditor dependencies from package.json.
+	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+
+	if ( dependencies ) {
+		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+
+		for ( let dependency in dependencies ) {
+			const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
+			let status;
+
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( dependency ) > -1 ) {
+				try {
+					status = git.getStatus( repositoryAbsolutePath );
+					writeln( `\x1b[1m\x1b[36m${ dependency }\x1b[0m\n${ status.trim() }` );
+				} catch ( error ) {
+					writeError( error );
+				}
+			}
+		}
+	} else {
+		writeln( 'No CKEditor5 dependencies found in package.json file.' );
+	}
+};

+ 50 - 0
dev/tasks/utils/dev-update.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const tools = require( './tools' );
+const git = require( './git' );
+const path = require( 'path' );
+
+/**
+ * 1. Get CKEditor5 dependencies from package.json file.
+ * 2. Scan workspace for repositories that match dependencies from package.json file.
+ * 3. Run GIT pull command on each repository found.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
+ * @param {Object} options grunt options.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, options, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, options.workspaceRoot );
+
+	// Get all CKEditor dependencies from package.json.
+	const dependencies = tools.getCKEditorDependencies( packageJSON.dependencies );
+
+	if ( dependencies ) {
+		const directories = tools.getCKE5Directories( workspaceAbsolutePath );
+
+		for ( let dependency in dependencies ) {
+			const repositoryURL = dependencies[ dependency ];
+			const urlInfo = git.parseRepositoryUrl( repositoryURL );
+			const repositoryAbsolutePath = path.join( workspaceAbsolutePath, dependency );
+
+			// Check if repository's directory already exists.
+			if ( directories.indexOf( dependency ) > -1 ) {
+				try {
+					writeln( `Updating ${ repositoryURL }...` );
+					git.pull( repositoryAbsolutePath, urlInfo.branch );
+				} catch ( error ) {
+					writeError( error );
+				}
+			}
+		}
+	} else {
+		writeln( 'No CKEditor5 dependencies found in package.json file.' );
+	}
+};

+ 154 - 0
dev/tasks/utils/git.js

@@ -0,0 +1,154 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const tools = require( './tools' );
+
+module.exports = {
+	/**
+	 * Holds boilerplate repository Git URL.
+	 *
+	 * @private
+	 * @readonly
+	 * @type {String}
+	 */
+	BOILERPLATE_REPOSITORY: 'git@github.com:ckeditor/ckeditor-boilerplate.git',
+
+	/**
+	 * Holds boilerplate branch used in CKEditor5 projects.
+	 *
+	 * @private
+	 * @readonly
+	 * @type {String}
+	 */
+	BOILERPLATE_BRANCH: 'ckeditor5',
+
+	/**
+	 * Parses GitHub URL. Extracts used server, repository and branch.
+	 *
+	 * @param {String} url GitHub URL from package.json file.
+	 * @returns {Object} urlInfo
+	 * @returns {String} urlInfo.server
+	 * @returns {String} urlInfo.repository
+	 * @returns {String} urlInfo.branch
+	 */
+	parseRepositoryUrl( url ) {
+		const regexp = /^(git@github\.com:|https?:\/\/github.com\/)?([^#]+)(?:#)?(.*)$/;
+		const match = url.match( regexp );
+		let server;
+		let repository;
+		let branch;
+
+		if ( !match ) {
+			return null;
+		}
+
+		server = match[ 1 ] || 'https://github.com/';
+		repository = match[ 2 ] || '';
+		branch = match[ 3 ] || 'master';
+
+		if ( !repository ) {
+			return null;
+		}
+
+		return {
+			server: server,
+			repository: repository,
+			branch: branch
+		};
+	},
+
+	/**
+	 * Clones repository to workspace.
+	 *
+	 * @param {Object} urlInfo Parsed URL object from {@link #parseRepositoryUrl}.
+	 * @param {String} workspacePath Path to the workspace location where repository will be cloned.
+	 */
+	cloneRepository( urlInfo, workspacePath ) {
+		const cloneCommands = [
+			`cd ${ workspacePath }`,
+			`git clone ${ urlInfo.server + urlInfo.repository }`
+		];
+
+		tools.shExec( cloneCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Checks out branch on selected repository.
+	 *
+	 * @param {String} repositoryLocation Absolute path to repository.
+	 * @param {String} branchName Name of the branch to checkout.
+	 */
+	checkout( repositoryLocation, branchName ) {
+		const checkoutCommands = [
+			`cd ${ repositoryLocation }`,
+			`git checkout ${ branchName }`
+		];
+
+		tools.shExec( checkoutCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Pulls specified branch from origin.
+	 *
+	 * @param {String} repositoryLocation Absolute path to repository.
+	 * @param {String} branchName Branch name to pull.
+	 */
+	pull( repositoryLocation, branchName ) {
+		const checkoutCommands = [
+			`cd ${ repositoryLocation }`,
+			`git pull origin ${ branchName }`
+		];
+
+		tools.shExec( checkoutCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Initializes new repository, adds and merges CKEditor5 boilerplate project.
+	 *
+	 * @param {String} repositoryPath Absolute path where repository should be created.
+	 */
+	initializeRepository( repositoryPath ) {
+		const initializeCommands = [
+			`git init ${ repositoryPath }`,
+			`cd ${ repositoryPath }`,
+			`git remote add boilerplate ${ this.BOILERPLATE_REPOSITORY }`,
+			`git fetch boilerplate ${ this.BOILERPLATE_BRANCH }`,
+			`git merge boilerplate/${ this.BOILERPLATE_BRANCH }`
+		];
+
+		tools.shExec( initializeCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Returns Git status of repository stored under specified path. It runs `git status --porcelain -sb` command.
+	 *
+	 * @param {String} repositoryPath Absolute path to repository.
+	 * @returns {String} Executed command's result.
+	 */
+	getStatus( repositoryPath ) {
+		return tools.shExec( `cd ${ repositoryPath } && git status --porcelain -sb` );
+	},
+
+	/**
+	 * Updates boilerplate project in specified repository.
+	 * @param {String} repositoryPath Absolute path to repository.
+	 */
+	updateBoilerplate( repositoryPath ) {
+		// Try to add boilerplate remote if one is not already added.
+		try {
+			tools.shExec( `cd ${ repositoryPath } && git remote add boilerplate ${ this.BOILERPLATE_REPOSITORY }` );
+		} catch ( error ) { }
+
+		const updateCommands = [
+			`cd ${ repositoryPath }`,
+			`git fetch boilerplate ${ this.BOILERPLATE_BRANCH }`,
+			`git merge boilerplate/${ this.BOILERPLATE_BRANCH }`
+		];
+
+		tools.shExec( updateCommands.join( ' && ' ) );
+	}
+};

+ 55 - 0
dev/tasks/utils/inquiries.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+const inquirer = require( 'inquirer' );
+const DEFAULT_PLUGIN_NAME_PREFIX = 'ckeditor5-plugin-';
+const DEFAULT_PLUGIN_VERSION = '0.0.1';
+const DEFAULT_GITHUB_URL_PREFIX = 'ckeditor/';
+
+module.exports = {
+	getPluginName() {
+		return new Promise( ( resolve ) => {
+			inquirer.prompt( [ {
+				name: 'pluginName',
+				message: 'Enter plugin name without ' + DEFAULT_PLUGIN_NAME_PREFIX + ' prefix:',
+				validate: ( input ) => {
+					var regexp = /^[a-zA-Z0-9-_]+$/;
+
+					return regexp.test( input ) ? true : 'Please provide a valid plugin name.';
+				}
+			} ], ( answers ) => {
+				resolve( DEFAULT_PLUGIN_NAME_PREFIX + answers.pluginName );
+			} );
+		} );
+	},
+
+	getPluginVersion( ) {
+		return new Promise( ( resolve ) => {
+			inquirer.prompt( [ {
+				name: 'version',
+				message: 'Enter plugin\'s initial version:',
+				default: DEFAULT_PLUGIN_VERSION
+			} ], ( answers ) => {
+				resolve( answers.version );
+			} );
+		} );
+	},
+
+	getPluginGitHubUrl( pluginName ) {
+		var defaultGitHubUrl = DEFAULT_GITHUB_URL_PREFIX + pluginName;
+
+		return new Promise( ( resolve ) => {
+			inquirer.prompt( [ {
+				name: 'gitHubUrl',
+				message: 'Enter plugin\'s GitHub URL:',
+				default: defaultGitHubUrl
+			} ], ( answers ) => {
+				resolve( answers.gitHubUrl );
+			} );
+		} );
+	}
+};

+ 85 - 41
dev/tasks/utils/tools.js

@@ -1,8 +1,10 @@
 'use strict';
 'use strict';
 
 
-var dirtyFiles,
+let dirtyFiles,
 	ignoreList;
 	ignoreList;
 
 
+const dependencyRegExp = /^ckeditor5-/;
+
 module.exports = {
 module.exports = {
 	/**
 	/**
 	 * Check if a task (including its optional target) is in the queue of tasks to be executed by Grunt.
 	 * Check if a task (including its optional target) is in the queue of tasks to be executed by Grunt.
@@ -11,7 +13,7 @@ module.exports = {
 	 * @param task {String} The task name. May optionally include the target (e.g. 'task:target').
 	 * @param task {String} The task name. May optionally include the target (e.g. 'task:target').
 	 * @returns {Boolean} "true" if the task is in the queue.
 	 * @returns {Boolean} "true" if the task is in the queue.
 	 */
 	 */
-	checkTaskInQueue: function( grunt, task ) {
+	checkTaskInQueue( grunt, task ) {
 		var cliTasks = grunt.cli.tasks;
 		var cliTasks = grunt.cli.tasks;
 
 
 		// Check if the task has been called directly.
 		// Check if the task has been called directly.
@@ -30,7 +32,7 @@ module.exports = {
 	 * @param grunt {Object} The Grunt object.
 	 * @param grunt {Object} The Grunt object.
 	 * @param options {Object} A list of options for the method. See the jscs and jshint tasks for example.
 	 * @param options {Object} A list of options for the method. See the jscs and jshint tasks for example.
 	 */
 	 */
-	setupMultitaskConfig: function( grunt, options ) {
+	setupMultitaskConfig( grunt, options ) {
 		var task = options.task;
 		var task = options.task;
 		var taskConfig = {};
 		var taskConfig = {};
 		var config = taskConfig[ task ] = {
 		var config = taskConfig[ task ] = {
@@ -73,7 +75,7 @@ module.exports = {
 	 * @param grunt {Object} The Grunt object.
 	 * @param grunt {Object} The Grunt object.
 	 * @returns {String[]} The list of ignores.
 	 * @returns {String[]} The list of ignores.
 	 */
 	 */
-	getGitIgnore: function( grunt ) {
+	getGitIgnore( grunt ) {
 		if ( !ignoreList ) {
 		if ( !ignoreList ) {
 			ignoreList = grunt.file.read( '.gitignore' );
 			ignoreList = grunt.file.read( '.gitignore' );
 
 
@@ -96,7 +98,7 @@ module.exports = {
 	 *
 	 *
 	 * @returns {String[]} A list of file paths.
 	 * @returns {String[]} A list of file paths.
 	 */
 	 */
-	getGitDirtyFiles: function() {
+	getGitDirtyFiles() {
 		// Cache it, so it is executed only once when running multiple tasks.
 		// Cache it, so it is executed only once when running multiple tasks.
 		if ( !dirtyFiles ) {
 		if ( !dirtyFiles ) {
 			dirtyFiles = this
 			dirtyFiles = this
@@ -122,11 +124,11 @@ module.exports = {
 	 * @param command {String} The command to be executed.
 	 * @param command {String} The command to be executed.
 	 * @returns {String} The command output.
 	 * @returns {String} The command output.
 	 */
 	 */
-	shExec: function( command ) {
-		var sh = require( 'shelljs' );
+	shExec( command ) {
+		const sh = require( 'shelljs' );
 		sh.config.silent = true;
 		sh.config.silent = true;
 
 
-		var ret = sh.exec( command );
+		const ret = sh.exec( command );
 
 
 		if ( ret.code ) {
 		if ( ret.code ) {
 			throw new Error(
 			throw new Error(
@@ -139,50 +141,32 @@ module.exports = {
 	},
 	},
 
 
 	/**
 	/**
-	 * Links repository located in source path to repository located in destination path. Uses npm link.
-	 * @param {String} sourcePath
-	 * @param {String} destinationPath
-	 * @param {String} pluginName
-	 */
-	npmLink: function( sourcePath, destinationPath, pluginName ) {
-		// Don't use sudo on windows when executing npm link.
-		var isWin = process.platform == 'win32';
-		var linkCommands = [
-			'cd ' + sourcePath,
-			( !isWin ? 'sudo ' : '' ) + 'npm link',
-			'cd ' + destinationPath,
-			'npm link ' + pluginName
-		];
-
-		module.exports.shExec( linkCommands.join( ' && ' ) );
-	},
-
-	/**
-	 * Clones repository from provided GitHub URL.
-	 * @param {String} gitHubUrl GitHub url to repository.
-	 * @param {String} location Destination path.
+	 * Links directory located in source path to directory located in destination path using `ln -s` command.
+	 * @param {String} source
+	 * @param {String} destination
 	 */
 	 */
-	cloneRepository: function( gitHubUrl, location ) {
-		var cloneCommands = [
-			'cd ' + location,
-			'git clone git@github.com:' + gitHubUrl
-		];
+	linkDirectories( source, destination ) {
+		// Remove destination directory if exists.
+		if ( this.isDirectory( destination ) ) {
+			this.shExec( `rm -rf ${ destination }` );
+		}
 
 
-		module.exports.shExec( cloneCommands.join( ' && ' ) );
+		this.shExec( `ln -s ${ source } ${ destination }` );
 	},
 	},
 
 
 	/**
 	/**
-	 * Returns dependencies that starts with ckeditor5- or null if no dependencies are found.
+	 * Returns dependencies that starts with ckeditor5-, and have valid, short GitHub url. Returns null if no
+	 * dependencies are found.
+	 *
 	 * @param {Object} dependencies Dependencies object loaded from package.json file.
 	 * @param {Object} dependencies Dependencies object loaded from package.json file.
 	 * @returns {Object|null}
 	 * @returns {Object|null}
 	 */
 	 */
-	getCKEditorDependencies: function( dependencies ) {
-		var result = null;
-		var regexp = /^ckeditor5-/;
+	getCKEditorDependencies( dependencies ) {
+		let result = null;
 
 
 		if ( dependencies ) {
 		if ( dependencies ) {
 			Object.keys( dependencies ).forEach( function( key ) {
 			Object.keys( dependencies ).forEach( function( key ) {
-				if ( regexp.test( key ) ) {
+				if ( dependencyRegExp.test( key ) ) {
 					if ( result === null ) {
 					if ( result === null ) {
 						result = {};
 						result = {};
 					}
 					}
@@ -193,5 +177,65 @@ module.exports = {
 		}
 		}
 
 
 		return result;
 		return result;
+	},
+
+	/**
+	 * 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 ) {
+		var fs = require( 'fs' );
+
+		try {
+			return fs.statSync( path ).isDirectory();
+		} catch ( e ) {}
+
+		return false;
+	},
+
+	/**
+	 * Returns all directories under specified path that match 'ckeditor5' pattern.
+	 *
+	 * @param {String} path
+	 * @returns {Array}
+	 */
+	getCKE5Directories( path ) {
+		return this.getDirectories( path ).filter( dir => {
+			return dependencyRegExp.test( dir );
+		} );
+	},
+
+	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 ), 'utf-8' );
+	},
+
+	npmInstall( path ) {
+		this.shExec( `cd ${ path } && npm install` );
+	},
+
+	installGitHooks( path ) {
+		this.shExec( `cd ${ path } && grunt githooks` );
 	}
 	}
 };
 };

+ 5 - 3
gruntfile.js

@@ -28,9 +28,11 @@ module.exports = function( grunt ) {
 			}
 			}
 		},
 		},
 
 
-		plugin: { },
-
-		dev: {}
+		dev: {
+			options: {
+				workspaceRoot: '..'
+			}
+		}
 	} );
 	} );
 
 
 	// Finally load the tasks.
 	// Finally load the tasks.

+ 2 - 2
package.json

@@ -20,7 +20,7 @@
     "benderjs-mocha": "^0.3.0",
     "benderjs-mocha": "^0.3.0",
     "benderjs-promise": "^0.1.0",
     "benderjs-promise": "^0.1.0",
     "benderjs-sinon": "^0.3.0",
     "benderjs-sinon": "^0.3.0",
-    "chai": "~1.10.0",
+    "chai": "^1.10.0",
     "del": "^2.0.2",
     "del": "^2.0.2",
     "grunt": "^0",
     "grunt": "^0",
     "grunt-jscs": "^2.0.0",
     "grunt-jscs": "^2.0.0",
@@ -42,6 +42,6 @@
     "url": "https://github.com/ckeditor/ckeditor5.git"
     "url": "https://github.com/ckeditor/ckeditor5.git"
   },
   },
   "scripts": {
   "scripts": {
-    "test": "mocha dev/tasks/test"
+    "test": "mocha dev/tasks/tests"
   }
   }
 }
 }