ソースを参照

Merge pull request #40 from ckeditor/t/36

T/36 - Tools for project related task.
Piotrek Koszuliński 10 年 前
コミット
433b7347c4

+ 55 - 0
dev/tasks/dev.js

@@ -0,0 +1,55 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+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 relinkTask = require( './utils/dev-relink' );
+const boilerplateUpdateTask = require( './utils/dev-boilerplate-update' );
+const ckeditor5Path = process.cwd();
+
+module.exports = ( grunt ) => {
+	const packageJSON = grunt.config.data.pkg;
+	const workspaceRoot = grunt.config.data.workspaceRoot;
+
+	grunt.registerTask( 'dev-init', function() {
+		initTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-plugin-create', function() {
+		const done = this.async();
+		pluginCreateTask( ckeditor5Path, workspaceRoot, grunt.log.writeln )
+			.then( done )
+			.catch( ( error )  => done( error ) );
+	} );
+
+	grunt.registerTask( 'dev-plugin-install', function() {
+		const done = this.async();
+		pluginInstallTask( ckeditor5Path, workspaceRoot, grunt.log.writeln )
+			.then( done )
+			.catch( ( error )  => done( error ) );
+	} );
+
+	grunt.registerTask( 'dev-update', function() {
+		pluginUpdateTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-status', function() {
+		pluginStatusTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-boilerplate-update', function() {
+		boilerplateUpdateTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
+	} );
+
+	grunt.registerTask( 'dev-relink', function() {
+		relinkTask( ckeditor5Path, packageJSON, workspaceRoot, grunt.log.writeln, grunt.log.error );
+	} );
+};
+

+ 2 - 0
dev/tasks/templates/CHANGES.md

@@ -0,0 +1,2 @@
+Changelog
+====================

+ 2 - 0
dev/tasks/templates/CONTRIBUTING.md

@@ -0,0 +1,2 @@
+Contributing
+============

+ 2 - 0
dev/tasks/templates/LICENSE.md

@@ -0,0 +1,2 @@
+Software License Agreement
+==========================

+ 2 - 0
dev/tasks/templates/README.md

@@ -0,0 +1,2 @@
+Development Repository
+===================================

+ 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 {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, 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. Pull changes from remote branch.
+ * 4. Link each new repository to node_modules. (do not use npm link, use standard linking instead)
+ * 5. Run `npm install` in each repository.
+ * 6. 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 {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, 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.
+			try {
+				if ( directories.indexOf( dependency ) === -1 ) {
+					writeln( `Clonning ${ repositoryURL }...` );
+					git.cloneRepository( urlInfo, workspaceAbsolutePath );
+				}
+
+				// Check out proper branch.
+				writeln( `Checking out ${ repositoryURL } to ${ urlInfo.branch }...` );
+				git.checkout( repositoryAbsolutePath, urlInfo.branch );
+
+				writeln( `Pulling changes to ${ repositoryURL } ${ urlInfo.branch }...` );
+				git.pull( 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. Copy template files.
+ * 6. Update package.json file in new plugin's repository.
+ * 7. Update package.json file in CKEditor5 repository.
+ * 8. Create initial commit.
+ * 9. Link new plugin.
+ * 10. Call `npm install` in plugin repository.
+ * 11. Install Git hooks in plugin repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @returns {Promise} Returns promise fulfilled after task is done.
+ */
+module.exports = ( ckeditor5Path, workspaceRoot, writeln ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
+	let pluginName;
+	let repositoryPath;
+	let pluginVersion;
+	let gitHubUrl;
+
+	return inquiries.getPluginName()
+		.then( result => {
+			pluginName = result;
+			repositoryPath = path.join( workspaceAbsolutePath, pluginName );
+
+			return inquiries.getPluginVersion();
+		} )
+		.then( result => {
+			pluginVersion = result;
+
+			return inquiries.getPluginGitHubUrl( pluginName );
+		} )
+		.then( result => {
+			gitHubUrl = result;
+
+			writeln( `Initializing repository ${ repositoryPath }...` );
+			git.initializeRepository( repositoryPath );
+
+			writeln( `Copying template files to ${ repositoryPath }...` );
+			tools.copyTemplateFiles( 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( `Creating initial commit...` );
+			git.initialCommit( pluginName, repositoryPath );
+
+			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 );
+		} );
+};

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

@@ -0,0 +1,70 @@
+/**
+ * @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 {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @returns {Promise} Returns promise fulfilled after task is done.
+ */
+module.exports = ( ckeditor5Path, workspaceRoot, writeln ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, workspaceRoot );
+	let pluginName;
+	let repositoryPath;
+	let gitHubUrl;
+
+	return inquiries.getPluginName()
+		.then( result => {
+			pluginName = result;
+			repositoryPath = path.join( workspaceAbsolutePath, pluginName );
+
+			return inquiries.getPluginGitHubUrl( pluginName );
+		} )
+		.then( result => {
+			gitHubUrl = result;
+			let urlInfo = git.parseRepositoryUrl( gitHubUrl );
+
+			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 );
+		} );
+};

+ 48 - 0
dev/tasks/utils/dev-relink.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 path = require( 'path' );
+
+/**
+ * 1. Get CKEditor5 dependencies from package.json file.
+ * 2. Scan workspace for repositories that match dependencies from package.json file.
+ * 3. Link repositories to node_modules in CKEditor5 repository.
+ *
+ * @param {String} ckeditor5Path Path to main CKEditor5 repository.
+ * @param {Object} packageJSON Parsed package.json file from CKEditor5 repository.
+ * @param {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, 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 );
+			const repositoryURL = dependencies[ dependency ];
+
+			// Check if repository's directory exists.
+			if ( directories.indexOf( dependency ) > -1 ) {
+				try {
+					writeln( `Linking ${ repositoryURL }...` );
+					tools.linkDirectories( repositoryAbsolutePath, path.join( ckeditor5Path, 'node_modules' , dependency ) );
+				} catch ( error ) {
+					writeError( error );
+				}
+			}
+		}
+	} else {
+		writeln( 'No CKEditor5 dependencies found in package.json file.' );
+	}
+};

+ 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 {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, 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 {String} workspaceRoot Relative path to workspace root.
+ * @param {Function} writeln Function for log output.
+ * @param {Function} writeError Function of error output
+ */
+module.exports = ( ckeditor5Path, packageJSON, workspaceRoot, writeln, writeError ) => {
+	const workspaceAbsolutePath = path.join( ckeditor5Path, 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.' );
+	}
+};

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

@@ -0,0 +1,172 @@
+/**
+ * @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 ) {
+		const regexp = /boilerplate(\n|$)/;
+
+		// Try to add boilerplate remote if one is not already added.
+		if ( !regexp.test( tools.shExec( `cd ${ repositoryPath } && git remote` ) ) ) {
+			tools.shExec( `cd ${ repositoryPath } && git remote add boilerplate ${ this.BOILERPLATE_REPOSITORY }` );
+		}
+
+		const updateCommands = [
+			`cd ${ repositoryPath }`,
+			`git fetch boilerplate ${ this.BOILERPLATE_BRANCH }`,
+			`git merge boilerplate/${ this.BOILERPLATE_BRANCH }`
+		];
+
+		tools.shExec( updateCommands.join( ' && ' ) );
+	},
+
+	/**
+	 * Creates initial commit on repository under specified path.
+	 *
+	 * @param {String} pluginName
+	 * @param {String} repositoryPath
+	 */
+	initialCommit( pluginName, repositoryPath ) {
+		const commitCommands = [
+			`cd ${ repositoryPath }`,
+			`git add .`,
+			`git commit -m "Initial commit for ${ pluginName }."`
+		];
+
+		tools.shExec( commitCommands.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 ) => {
+					const regexp = /^[\w-]+$/;
+
+					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 ) {
+		const 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 );
+			} );
+		} );
+	}
+};

+ 130 - 0
dev/tasks/utils/tools.js

@@ -3,6 +3,9 @@
 let dirtyFiles,
 	ignoreList;
 
+const dependencyRegExp = /^ckeditor5-/;
+const TEMPLATE_PATH = './dev/tasks/templates';
+
 module.exports = {
 	/**
 	 * Check if a task (including its optional target) is in the queue of tasks to be executed by Grunt.
@@ -136,5 +139,132 @@ module.exports = {
 		}
 
 		return ret.output;
+	},
+
+	/**
+	 * 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.isDirectory( destination ) ) {
+			this.shExec( `rm -rf ${ destination }` );
+		}
+
+		fs.symlinkSync( source, destination, 'dir' );
+	},
+
+	/**
+	 * 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.
+	 * @returns {Object|null}
+	 */
+	getCKEditorDependencies( dependencies ) {
+		let result = null;
+
+		if ( dependencies ) {
+			Object.keys( dependencies ).forEach( function( key ) {
+				if ( dependencyRegExp.test( key ) ) {
+					if ( result === null ) {
+						result = {};
+					}
+
+					result[ key ] = dependencies[ key ];
+				}
+			} );
+		}
+
+		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 ) {
+		const 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 );
+		} );
+	},
+
+	/**
+	 * 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 ), 'utf-8' );
+	},
+
+	/**
+	 * Calls `npm install` command in specified path.
+	 *
+	 * @param {String} path
+	 */
+	npmInstall( path ) {
+		this.shExec( `cd ${ path } && npm install` );
+	},
+
+	/**
+	 * Installs Git hooks in specified repository.
+	 *
+	 * @param {String} path
+	 */
+	installGitHooks( path ) {
+		this.shExec( `cd ${ path } && grunt githooks` );
+	},
+
+	/**
+	 * Copies template files to specified destination.
+	 *
+	 * @param {String} destination
+	 */
+	copyTemplateFiles( destination ) {
+		const path = require( 'path' );
+		const templatesPath = path.resolve( TEMPLATE_PATH );
+		this.shExec( `cp ${ path.join( templatesPath, '*.md' ) } ${ destination }` );
 	}
 };

+ 322 - 0
dev/tests/dev-tasks.js

@@ -0,0 +1,322 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const tools = require( '../tasks/utils/tools' );
+const inquiries = require( '../tasks/utils/inquiries' );
+const git = require( '../tasks/utils/git' );
+const path = require( 'path' );
+const emptyFn = () => { };
+let spies;
+
+describe( 'dev-tasks', () => {
+	const mainRepositoryPath = '/path/to/repository';
+	const workspaceRoot = '..';
+	const workspacePath = path.join( mainRepositoryPath, workspaceRoot );
+	const pluginName = 'plugin-name';
+	const repositoryPath = path.join( workspacePath, pluginName );
+	const pluginVersion = '0.0.1';
+	const gitHubUrl = 'ckeditor5/plugin-name';
+
+	beforeEach( () => createSpies() );
+	afterEach( () => restoreSpies() );
+
+	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' ),
+			pull: sinon.stub( git, 'pull' ),
+			checkout: sinon.stub( git, 'checkout' ),
+			npmInstall: sinon.stub( tools, 'npmInstall' ),
+			installGitHooks: sinon.stub( tools, 'installGitHooks' ),
+			getPluginName: sinon.stub( inquiries, 'getPluginName' ).returns( new Promise( ( r ) => r( pluginName ) ) ),
+			getPluginVersion: sinon.stub( inquiries, 'getPluginVersion' ).returns( new Promise( ( r ) => r( pluginVersion ) ) ),
+			getPluginGitHubUrl: sinon.stub( inquiries, 'getPluginGitHubUrl' ).returns( new Promise( ( r ) => r( gitHubUrl ) ) ),
+			initializeRepository: sinon.stub( git, 'initializeRepository' ),
+			updateJSONFile: sinon.stub( tools, 'updateJSONFile' ),
+			getStatus: sinon.stub( git, 'getStatus' ),
+			updateBoilerplate: sinon.stub( git, 'updateBoilerplate' ),
+			copyTemplateFiles: sinon.stub( tools, 'copyTemplateFiles' ),
+			initialCommit: sinon.stub( git, 'initialCommit' )
+		};
+	}
+
+	function restoreSpies() {
+		for ( let spy in spies ) {
+			spies[ spy ].restore();
+		}
+	}
+
+	describe( 'dev-init', () => {
+		const initTask = require( '../tasks/utils/dev-init' );
+
+		it( 'task should exist', () => 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, workspaceRoot, 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.pull.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, workspaceRoot, 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, 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.pull.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, workspaceRoot, 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, workspaceRoot ) );
+			expect( spies.parseRepositoryUrl.calledTwice ).to.equal( true );
+			expect( spies.cloneRepository.called ).to.equal( false );
+			expect( spies.checkout.calledTwice ).to.equal( true );
+			expect( spies.pull.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 );
+		} );
+	} );
+
+	describe( 'dev-plugin-create', () => {
+		const pluginCreateTask = require( '../tasks/utils/dev-plugin-create' );
+		const repositoryPath = path.join( workspacePath, pluginName );
+
+		it( 'should exist', () => expect( pluginCreateTask ).to.be.a( 'function' ) );
+
+		it( 'should create a plugin', () => {
+			return pluginCreateTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
+				expect( spies.getPluginName.calledOnce ).to.equal( true );
+				expect( spies.getPluginVersion.calledOnce ).to.equal( true );
+				expect( spies.getPluginGitHubUrl.calledOnce ).to.equal( true );
+				expect( spies.initializeRepository.calledOnce ).to.equal( true );
+				expect( spies.initializeRepository.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.copyTemplateFiles.calledOnce ).to.equal( true );
+				expect( spies.copyTemplateFiles.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.updateJSONFile.calledTwice ).to.equal( true );
+				expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( repositoryPath, 'package.json' ) );
+				expect( spies.updateJSONFile.secondCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
+				expect( spies.initialCommit.calledOnce ).to.equal( true );
+				expect( spies.initialCommit.firstCall.args[ 0 ] ).to.equal( pluginName );
+				expect( spies.initialCommit.firstCall.args[ 1 ] ).to.equal( repositoryPath );
+				expect( spies.linkDirectories.calledOnce ).to.equal( true );
+				expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', pluginName ) );
+				expect( spies.npmInstall.calledOnce ).to.equal( true );
+				expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.installGitHooks.calledOnce ).to.equal( true );
+				expect( spies.installGitHooks.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			} );
+		} );
+	} );
+
+	describe( 'dev-plugin-install', () => {
+		const pluginInstallTask = require( '../tasks/utils/dev-plugin-install' );
+
+		it( 'should exist', () => expect( pluginInstallTask ).to.be.a( 'function' ) );
+
+		it( 'should install a plugin', () => {
+			return pluginInstallTask( mainRepositoryPath, workspaceRoot, emptyFn ).then( () => {
+				expect( spies.getPluginName.calledOnce ).to.equal( true );
+				expect( spies.getPluginGitHubUrl.calledOnce ).to.equal( true );
+				expect( spies.parseRepositoryUrl.calledOnce ).to.equal( true );
+				const urlInfo = spies.parseRepositoryUrl.firstCall.returnValue;
+				expect( spies.parseRepositoryUrl.firstCall.args[ 0 ] ).to.equal( gitHubUrl );
+				expect( spies.cloneRepository.calledOnce ).to.equal( true );
+				expect( spies.cloneRepository.firstCall.args[ 0 ] ).to.equal( urlInfo );
+				expect( spies.cloneRepository.firstCall.args[ 1 ] ).to.equal( workspacePath );
+				expect( spies.checkout.calledOnce ).to.equal( true );
+				expect( spies.checkout.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.checkout.firstCall.args[ 1 ] ).to.equal( urlInfo.branch );
+				expect( spies.updateJSONFile.calledOnce ).to.equal( true );
+				expect( spies.updateJSONFile.firstCall.args[ 0 ] ).to.equal( path.join( mainRepositoryPath, 'package.json' ) );
+				expect( spies.linkDirectories.calledOnce ).to.equal( true );
+				expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', pluginName ) );
+				expect( spies.npmInstall.calledOnce ).to.equal( true );
+				expect( spies.npmInstall.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+				expect( spies.installGitHooks.calledOnce ).to.equal( true );
+				expect( spies.installGitHooks.firstCall.args[ 0 ] ).to.equal( repositoryPath );
+			} );
+		} );
+	} );
+
+	describe( 'dev-relink', () => {
+		const devRelinkTask = require( '../tasks/utils/dev-relink' );
+
+		it( 'should exist', () => expect( devRelinkTask ).to.be.a( 'function' ) );
+
+		it( 'should relink repositories', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			spies.getDirectories.restore();
+			const dirs = [ 'ckeditor5-core', 'ckeditor5-plugin-devtest' ];
+			spies.getDirectories = sinon.stub( tools, 'getCKE5Directories', () => dirs );
+
+			devRelinkTask( mainRepositoryPath, packageJSON, workspaceRoot, emptyFn, emptyFn );
+
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.linkDirectories.calledTwice ).to.equal( true );
+			expect( spies.linkDirectories.firstCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 0 ] ) );
+			expect( spies.linkDirectories.firstCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', dirs[ 0 ] ) );
+			expect( spies.linkDirectories.secondCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 1 ] ) );
+			expect( spies.linkDirectories.secondCall.args[ 1 ] ).to.equal( path.join( mainRepositoryPath, 'node_modules', dirs[ 1 ] ) );
+		} );
+	} );
+
+	describe( 'dev-status', () => {
+		const devStatusTask = require( '../tasks/utils/dev-status' );
+
+		it( 'should exist', () => expect( devStatusTask ).to.be.a( 'function' ) );
+
+		it( 'should show repositories status', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			const dirs = [ 'ckeditor5-core', 'ckeditor5-plugin-devtest' ];
+			spies.getDirectories.restore();
+			spies.getDirectories = sinon.stub( tools, 'getCKE5Directories', () => dirs );
+
+			devStatusTask( mainRepositoryPath, packageJSON, workspaceRoot, emptyFn, emptyFn );
+
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.getStatus.calledTwice ).to.equal( true );
+			expect( spies.getStatus.firstCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 0 ] ) );
+			expect( spies.getStatus.secondCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 1 ] ) );
+		} );
+	} );
+
+	describe( 'dev-update', () => {
+		const devUpdateTask = require( '../tasks/utils/dev-update' );
+
+		it( 'should exist', () => expect( devUpdateTask ).to.be.a( 'function' ) );
+
+		it( 'should show repositories status', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			const dirs = [ 'ckeditor5-core', 'ckeditor5-plugin-devtest' ];
+			spies.getDirectories.restore();
+			spies.getDirectories = sinon.stub( tools, 'getCKE5Directories', () => dirs );
+
+			devUpdateTask( mainRepositoryPath, packageJSON, workspaceRoot, emptyFn, emptyFn );
+
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.parseRepositoryUrl.calledTwice ).to.equal( true );
+			expect( spies.pull.calledTwice ).to.equal( true );
+
+			let urlInfo = spies.parseRepositoryUrl.firstCall.returnValue;
+			expect( spies.pull.firstCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 0 ] ) );
+			expect( spies.pull.firstCall.args[ 1 ] ).to.equal( urlInfo.branch );
+
+			urlInfo = spies.parseRepositoryUrl.secondCall.returnValue;
+			expect( spies.pull.secondCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 1 ] ) );
+			expect( spies.pull.secondCall.args[ 1 ] ).to.equal( urlInfo.branch );
+		} );
+	} );
+
+	describe( 'dev-boilerplate-update', () => {
+		const devBoilerplateTask = require( '../tasks/utils/dev-boilerplate-update' );
+
+		it( 'should exist', () => expect( devBoilerplateTask ).to.be.a( 'function' ) );
+
+		it( 'should update boilerplate in repositories', () => {
+			const packageJSON = {
+				dependencies: {
+					'ckeditor5-core': 'ckeditor/ckeditor5-core',
+					'ckeditor5-plugin-devtest': 'ckeditor/ckeditor5-plugin-devtest',
+					'non-ckeditor-plugin': 'other/plugin'
+				}
+			};
+
+			const dirs = [ 'ckeditor5-core', 'ckeditor5-plugin-devtest' ];
+			spies.getDirectories.restore();
+			spies.getDirectories = sinon.stub( tools, 'getCKE5Directories', () => dirs );
+
+			devBoilerplateTask( mainRepositoryPath, packageJSON, workspaceRoot, emptyFn, emptyFn );
+
+			expect( spies.getDependencies.calledOnce ).to.equal( true );
+			expect( spies.getDependencies.firstCall.args[ 0 ] ).to.equal( packageJSON.dependencies );
+			expect( spies.updateBoilerplate.calledTwice ).to.equal( true );
+			expect( spies.updateBoilerplate.firstCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 0 ] ) );
+			expect( spies.updateBoilerplate.secondCall.args[ 0 ] ).to.equal( path.join( workspacePath, dirs[ 1 ] ) );
+		} );
+	} );
+} );

+ 238 - 0
dev/tests/git.js

@@ -0,0 +1,238 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+let toRestore;
+const git = require( '../tasks/utils/git' );
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const tools = require( '../tasks/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 fetch and merge boilerplate if remote already exists', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryLocation = 'path/to/repository';
+				const updateCommands = [
+					`cd ${ repositoryLocation }`,
+					`git fetch boilerplate ${ git.BOILERPLATE_BRANCH }`,
+					`git merge boilerplate/${ git.BOILERPLATE_BRANCH }`
+				];
+				shExecStub.onCall( 0 ).returns( 'origin\nboilerplate' );
+				toRestore.push( shExecStub );
+
+				git.updateBoilerplate( repositoryLocation );
+
+				expect( shExecStub.calledTwice ).to.equal( true );
+				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( updateCommands.join( ' && ' ) );
+			} );
+
+			it( 'should add boilerplate remote if one not exists', () => {
+				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 }`
+				];
+				shExecStub.onCall( 0 ).returns( 'origin\nnew' );
+				toRestore.push( shExecStub );
+
+				git.updateBoilerplate( repositoryLocation );
+
+				expect( shExecStub.calledThrice ).to.equal( true );
+				expect( shExecStub.secondCall.args[ 0 ] ).to.equal( addRemoteCommands );
+				expect( shExecStub.thirdCall.args[ 0 ] ).to.equal( updateCommands.join( ' && ' )  );
+			} );
+		} );
+
+		describe( 'initialCommit', () => {
+			it( 'should be defined', () => expect( git.initialCommit ).to.be.a( 'function' ) );
+			it( 'should execute commit commands', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const pluginName = 'ckeditor5-plugin-name';
+				const repositoryPath = '/path/to/repo';
+				const commitCommands = [
+					`cd ${ repositoryPath }`,
+					`git add .`,
+					`git commit -m "Initial commit for ${ pluginName }."`
+				];
+				toRestore.push( shExecStub );
+
+				git.initialCommit( pluginName, repositoryPath );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( commitCommands.join( ' && ' ) );
+			} );
+		} );
+	} );
+} );

+ 237 - 0
dev/tests/tools.js

@@ -0,0 +1,237 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global describe, it, beforeEach, afterEach */
+
+'use strict';
+
+const chai = require( 'chai' );
+const sinon = require( 'sinon' );
+const expect = chai.expect;
+const tools = require( '../tasks/utils/tools' );
+const path = require( 'path' );
+const fs = require( 'fs' );
+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 link directories', () => {
+				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( false );
+				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const source = '/source/dir';
+				const destination = '/destination/dir';
+				toRestore.push( symlinkStub, isDirectoryStub );
+
+				tools.linkDirectories( source, destination );
+
+				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( symlinkStub.calledOnce ).to.equal( true );
+				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
+				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( destination );
+			} );
+
+			it( 'should remove destination directory before linking', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const isDirectoryStub = sinon.stub( tools, 'isDirectory' ).returns( true );
+				const symlinkStub = sinon.stub( fs, 'symlinkSync' );
+				const source = '/source/dir';
+				const destination = '/destination/dir';
+				toRestore.push( symlinkStub, shExecStub, isDirectoryStub );
+
+				tools.linkDirectories( source, destination );
+
+				expect( isDirectoryStub.calledOnce ).to.equal( true );
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( symlinkStub.firstCall.args[ 0 ] ).to.equal( source );
+				expect( symlinkStub.firstCall.args[ 1 ] ).to.equal( 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' );
+			} );
+		} );
+
+		describe( 'updateJSONFile', () => {
+			it( 'should be defined', () => expect( tools.updateJSONFile ).to.be.a( 'function' ) );
+			it( 'should read, update and save JSON file', () => {
+				const path = 'path/to/file.json';
+				const fs = require( 'fs' );
+				const readFileStub = sinon.stub( fs, 'readFileSync', () => '{}' );
+				const modifiedJSON = { modified: true };
+				const writeFileStub = sinon.stub( fs, 'writeFileSync' );
+				toRestore.push( readFileStub, writeFileStub );
+
+				tools.updateJSONFile( path, () => {
+					return modifiedJSON;
+				} );
+
+				expect( readFileStub.calledOnce ).to.equal( true );
+				expect( readFileStub.firstCall.args[ 0 ] ).to.equal( path );
+				expect( writeFileStub.calledOnce ).to.equal( true );
+				expect( writeFileStub.firstCall.args[ 0 ] ).to.equal( path );
+				expect( writeFileStub.firstCall.args[ 1 ] ).to.equal( JSON.stringify( modifiedJSON, null, 2 ) );
+			} );
+		} );
+
+		describe( 'npmInstall', () => {
+			it( 'should be defined', () => expect( tools.npmInstall ).to.be.a( 'function' ) );
+			it( 'should execute npm install command', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const path = '/path/to/repository';
+				toRestore.push( shExecStub );
+
+				tools.npmInstall( path );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cd ${ path } && npm install` );
+			} );
+		} );
+
+		describe( 'installGitHooks', () => {
+			it( 'should be defined', () => expect( tools.installGitHooks ).to.be.a( 'function' ) );
+			it( 'should execute grunt githooks command', () => {
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const path = '/path/to/repository';
+				toRestore.push( shExecStub );
+
+				tools.installGitHooks( path );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cd ${ path } && grunt githooks` );
+			} );
+		} );
+
+		describe( 'copyTemplateFiles', () => {
+			it( 'should be defined', () => expect( tools.copyTemplateFiles ).to.be.a( 'function' ) );
+			it( 'should copy template files', () => {
+				const path = require( 'path' );
+				const TEMPLATE_PATH = './dev/tasks/templates';
+				const templatesPath = path.resolve( TEMPLATE_PATH );
+				const shExecStub = sinon.stub( tools, 'shExec' );
+				const repositoryPath = '/path/to/repository';
+				toRestore.push( shExecStub );
+
+				tools.copyTemplateFiles( repositoryPath );
+
+				expect( shExecStub.calledOnce ).to.equal( true );
+				expect( shExecStub.firstCall.args[ 0 ] ).to.equal( `cp ${ path.join( templatesPath, '*.md' ) } ${ repositoryPath }` );
+			} );
+		} );
+	} );
+} );

+ 17 - 0
gruntfile.js

@@ -2,6 +2,8 @@
 
 'use strict';
 
+const tools = require( './dev/tasks/utils/tools' );
+
 module.exports = ( grunt ) => {
 	// First register the "default" task, so it can be analyzed by other tasks.
 	grunt.registerTask( 'default', [ 'jshint:git', 'jscs:git' ] );
@@ -15,6 +17,7 @@ module.exports = ( grunt ) => {
 	// Basic configuration which will be overloaded by the tasks.
 	grunt.initConfig( {
 		pkg: grunt.file.readJSON( 'package.json' ),
+		workspaceRoot: '..',
 
 		jshint: {
 			options: {
@@ -26,9 +29,23 @@ module.exports = ( grunt ) => {
 			options: {
 				excludeFiles: ignoreFiles
 			}
+		},
+
+		replace: {
+			copyright: {
+				src: [ '**/*.*', '**/*.frag' ].concat( tools.getGitIgnore( grunt ).map( i => '!' + i ) )  ,
+				overwrite: true,
+				replacements: [
+					{
+						from: /\@license Copyright \(c\) 2003-\d{4}, CKSource - Frederico Knabben\./,
+						to: '@license Copyright (c) 2003-<%= grunt.template.today("yyyy") %>, CKSource - Frederico Knabben.'
+					}
+				]
+			}
 		}
 	} );
 
 	// Finally load the tasks.
 	grunt.loadTasks( 'dev/tasks' );
+	grunt.loadNpmTasks( 'grunt-text-replace' );
 };

+ 10 - 2
package.json

@@ -20,14 +20,19 @@
     "benderjs-mocha": "^0.3.0",
     "benderjs-promise": "^0.1.0",
     "benderjs-sinon": "^0.3.0",
+    "chai": "^1.10.0",
     "del": "^2.0.2",
     "grunt": "^0",
-    "grunt-jscs": "^2.0.0",
     "grunt-contrib-jshint": "^0",
     "grunt-githooks": "^0",
+    "grunt-jscs": "^2.0.0",
+    "grunt-text-replace": "^0.4.0",
+    "inquirer": "^0.11.0",
+    "mocha": "^2.2.5",
     "ncp": "^2.0.0",
     "replace": "^0.3.0",
-    "shelljs": "^0"
+    "shelljs": "^0",
+    "sinon": "^1.17.0"
   },
   "author": "CKSource (http://cksource.com/)",
   "license": "See LICENSE.md",
@@ -36,5 +41,8 @@
   "repository": {
     "type": "git",
     "url": "https://github.com/ckeditor/ckeditor5.git"
+  },
+  "scripts": {
+    "tests": "mocha dev/tests"
   }
 }