Repository: phase2/grunt-drupal-tasks (js)

Issue:
Add drush updb and fra as an update task
I recently experienced a situation where I merged in a branch that contained an update hook. I ran 'grunt'  to rebuild the site, but the site failed due to a missing table column. I think it'd be worth adding an update task that can run on site builds. This would be handy as code is deployed to various environments.

Pull request description:
Project Operations via the scripts mechanism
This change adapts the mechanism used for the new Theme Scripts system to support simple project-level operations via simple Gruntconfig.json settings. These can be one-liners entirely contained in the configuration, or they could call out to shell scripts elsewhere in the repository.

If you configure some scripts and run `grunt help` you will see each script displayed in the Operations section.

The documentation has had a first pass, but I think it's getting confusing. Also, I think this needs a bit more test coverage, hence the WIP flag.

I've deliberately avoided setting any default scripts, but there's a lot of potential for gadget to generate a set of good starting ones by prompting for a profile name and maybe a couple other questions. I'll file some issues in the gadget queue based on this once we get it in.

Fixes #156, #98.


Patch:
diff --git a/CONFIG.md b/CONFIG.md
index f6595d5..ce284a4 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -181,7 +181,8 @@ not default to hitting a production or staging environment.
 **siteUrls**: (Optional) A map of Drupal subsite names to the URLs by which
 each can be accessed for end-to-end testing by tools such as Behat. For clarity
 please align the site names (keys) with any Drush Site Aliases. If no siteUrls
-are specified it will default to `http://<%= config.domain %>`.
+are specified it will default to `http://<%= config.domain %>`. The environment
+variable override is `GDT_SITEURLS`.
 
 ```
   "siteUrls": {
@@ -321,10 +322,11 @@ general project tasks.
 }
 ```
 
-`grunt themes` may be run to list all themes setup in your project Gruntconfig.json.
-`grunt themes:\<theme\>` will list all the options in the theme's scripts config
-as a means of providing usage documentation. `grunt themes:\<theme\>:\<task\>`
-will run the script keyed by `\<task\>` from the theme's directory.
+`grunt themes` may be run to list all themes setup in your project
+Gruntconfig.json. `grunt themes:\<theme\>` will list all the options in the
+theme's scripts config as a means of providing usage documentation.
+`grunt themes:\<theme\>:\<task\>` will run the script keyed by `\<task\>` from
+the theme's directory.
 
 Three of the key names are magic: 'compile-theme', 'validate', and 'analyze'.
 When one of those three is specified, Grunt Drupal Tasks will automatically
@@ -334,6 +336,9 @@ run the command as part of the actions it takes for `grunt compile-theme`,
 Note that if you also configure compass for the theme, the existence of a
 "compile-theme" script will be run alongside the Grunt Drupal Tasks compass.
 
+Note that Theme Scripts support the `pre-` and `post-` operations explained in
+Project Operations.
+
 #### Setting Up Gem Dependencies
 
 In order for compass compilation to work, you will need to create a Gemfile
@@ -453,9 +458,10 @@ needed to run Drupal in a formal environment such as Production.
 
 You can find the resulting package in `build/packages/package` by default as a
 standard directory, all symlinks from the grunt scaffolding dereferenced. If
-run with `grunt package:compress` it will also output `build/packages/package.tgz`
-as an easily stored archive. **Remember, this directory is wiped by `grunt clean`
-unless you configure your package directory to be outside the build directory.**
+run with `grunt package:compress` it will also output
+`build/packages/package.tgz` as an easily stored archive. **Remember, this
+directory is wiped by `grunt clean` unless you configure your package directory
+to be outside the build directory.**
 
 This is an example of the settings for package tasks:
 
@@ -477,17 +483,59 @@ this format, see: http://gruntjs.com/configuring-tasks#files
 from the project directory when building a package. The above includes README
 files and files under bin/ in the project's package.
 
-**packages.dest.docroot**: Specify where within the package directory the `srcFiles`
-should be placed. Defaults to the package root. For Acquia set this to
-'/docroot'.
+**packages.dest.docroot**: Specify where within the package directory the
+`srcFiles` should be placed. Defaults to the package root. For Acquia set this
+to '/docroot'.
 
 **packages.dest.devResources**: Specify where within the package directory the
 `projFiles` should be placed. Defaults to package root.
 
+### Project Operations
+
+Some of the most common use cases for any Drupal site are the basic operations
+of installation, running database updates, and clearing the cache. These are not
+difficult operations, but many developers appreciate the common shorthand a
+little grunt magic can provide.
+
+To keep this flexible the new Operations system offers nothing out of the box,
+but facilitates creating lightweight commands or callouts to the shell for more
+advanced scripts you want easily run on the project by the entire team.
+
+This should remind you of the **Themes Scripts** described above.
+
+```
+{
+  "scripts": {
+    "update": "drush <%= config.alias %> updatedb -yv && drush <%= config.alias %> features-revert-all -yv",
+    "reset": "drush <%= config.alias %> registry-rebuild && drush <%= config.alias %> cc all"
+  }
+}
+```
+
+Here you see two commands defined inside the top-level `scripts` configuration.
+
+* `grunt update` demonstrates running database updates and reverting all
+[Features](http://www.drupal.org/project/features).
+* `grunt reset` demonstrates a thorough reset of the file registry and Drupal
+caches.
+
+The Alias and Profile is specified per the rules for **Magic Configuration**.
+
+**alias**: The Drush Site Alias. This is computed as a helper for use in
+project operations, but is not yet applied as part of other tasks. Defaults to
+`@<domain>`. The environment variable to override is `GDT_SITE_ALIAS`.
+
+#### Pre and Post Operations
+
+Similar to `npm run`, the operations defined in your Gruntconfig will support
+`pre-` and `post-` variants. If the main script exists the pre-script will be
+triggered first and the post-script will be triggered after. This allows you to
+easily extend any project operation with separate grunt, bash, or other tasks.
+
 ### Install settings
 
-The `install` task will either install Drupal via the configured `profile`, or if
-a path to a database is configured, load this database:
+The `install` task will either install Drupal via the configured `profile`, or
+if a path to a database is configured, load this database:
 
 To specify a profile other than the `standard` profile:
 
@@ -495,6 +543,8 @@ To specify a profile other than the `standard` profile:
 {
   "project": {
     "profile": "openatrium"
+  }
+}
 ```
 
 to specify a database to load:
@@ -503,11 +553,24 @@ to specify a database to load:
 {
   "project": {
     "db": "path/to/project.sql.gz"
+  }
+}
 ```
 
-**project.profile** The profile to use with `drush:site-install` internal task. Defaults to `standard` and may be overridden
-with `--profile` when the install task is run.
-**project.db** If specified, this database will be loaded instead of running a site installation.
+**project.profile** The Drupal installation profile, this will be used to
+configure the behavior of the `grunt install` and `grunt serve` tasks. Defaults
+to `standard`. The environment variable is `GDT_INSTALL_PROFILE`
+
+**project.db** If specified, this database will be loaded instead of running a
+site installation. If the file referenced is not present `grunt install` will
+fall back to a standard `drush site-install`.
+
+#### Pre and Post Installation
+
+The install task has a unique implementation of support for the pre and post
+scripts otherwise made available for themes and project operations. This allows
+you to implement simple directives or scripts to prepare for installation or
+perform non-standardized operations after the main installation routine.
 
 ### Serve Settings
 
@@ -529,55 +592,98 @@ skip starting up watch tasks.
     "concurrent": [
       "watch-theme"
     ]
-
+  }
 }
 ```
 
-**serve.profile**: \[DEPRECATED - use **project.profile** instead\] The profile to use with the drush:liteinstall task. Defaults
-to `standard` and may be overridden with `--profile` when the command is run.
-WARNING: drush:liteinstall is an internal task and is likely to be deprecated in
-a future release.
+**serve.profile**: DEPRECATED - use **project.profile** instead.
 
 **serve.port**: The port number to bind for the webserver. Only one service may
-occupy a port on a machine, so a project-specific port may be worthwhile. Defaults
-to `8080`.
+occupy a port on a machine, so a project-specific port may be worthwhile.
+Defaults to `8080`.
 
-**serve.concurrent**: An array of grunt tasks to be run concurrently to the server.
-When run with `serve:demo` or `serve:test` these tasks are not used. By default
-they include 'watch-test' and 'watch-theme'. If you use this configuration to
-add tasks be sure to include these as they will be suppressed by any configuration.
+**serve.concurrent**: An array of grunt tasks to be run concurrently to the
+server. When run with `serve:demo` or `serve:test` these tasks are not used. By
+default they include 'watch-test' and 'watch-theme'. If you use this
+configuration to add tasks be sure to include these as they will be suppressed
+by any configuration.
 
-### Help Settings (Help API)
+### Magic Configuration
 
-If you add custom tasks to your project and want them exposed as part of the
-`help` task, you may add a simple code snippet to your Gruntfile.js or any loaded
-task file.
+The domain, project.profile, alias, and siteUrls configurations are all built
+using the same system that allows for maximum flexibility in project and
+environment configuration. Note that all configuration is subject to the Project
+Gruntfile override.
 
-```js
-var Help = require('grunt-drupal-tasks/lib/help');
+In order of precedence it checks the following:
 
-Help.addItem('existing-task', 'Named Group', 'Optional description that avoids the default task description.');
+#### Priority 1: Project Gruntfile.js
 
-Help.add({
-  task: 'existing-task',
-  group: 'Named Group',
-  description: 'Optional description that avoids the default task description.'
-});
+The value of the configuration prefixed with `gdt.`. This allows a project
+Gruntfile to dynamically specify the value before bootstrapping Grunt Drupal
+Tasks. This is valuable because the code of grunt-drupal-tasks sets up many
+behaviors based on that configuration that would be difficult to replicate
+after the bootstrap.
 
-Help.add([
-  {
-    task: 'existing-task',
-    group: 'Named Group',
-    description: 'Optional description that avoids the default task description.'
-  },
-  {
-    task: 'second-task',
-    group: 'Named Group',
-    description: 'A second registered task to register with the help system.'
-  }
-]);
+This can be done with any configuration you might place into your
+Gruntconfig.json, and is merged recursively as an override to whatever you place
+in that file.
+
+For example:
+
+  * **alias**: `gdt.alias`
+  * **domain**: `gdt.domain`
+  * **profile**: `gdt.profile`
+  * **siteUrls**: `gdt.siteUrls`
+
+Suppose your environment is set up with the following directory structure:
+
+>  * project.example.com
+    * project-repository
+      * Gruntfile.js
+      * Gruntconfig.json
+      * src/
+      * ...
+   * project2.example.net
+     * ...
+
+Given this structure, you will not want to use environment variables because
+each of your projects might need different values in the host machine. However,
+a little logic in the project Gruntfile.js can allow you to dynamically derive
+your development domain or Drush site alias.
+
+For example, here is a Gruntfile that derives the site alias and domain
+based on the parent directory name of the project repository.
+
+```js
+var path = require('path');
+module.exports = function(grunt) {
+  var name = path.basename(path.resolve(__dirname, '..'));
+  // Set domain to "project.example.com". If nothing else sets the `alias`
+  // it will default to @project.example.com.
+  grunt.initConfig({ gdt: { domain: name });
+  // Load all plugins and tasks defined by the grunt-drupal-tasks package.
+  require('grunt-drupal-tasks')(grunt);
+};
 ```
 
+#### Priority 2: Environment Variable
+
+Each magic configuration property can also be set via an Environment Variable.
+
+Environment Variables are extremely powerful, but only useful where the
+environment contains only a single project using Grunt Drupal Tasks. This is
+more effective for dedicated environments, such as official production and
+staging servers, or virtual and containerized environments.
+
+The specific environment variable name is covered as part of the property
+descriptions.
+
+#### Priority 3: Project Gruntconfig.json
+
+The configuration placed in Gruntconfig.json. This is good for straightforward
+defaults and is how all *non-magical* configuration works.
+
 If you want to include your task in one of the existing groups, copy the text
 exactly as seen in the output of the `grunt help` task.
 
@@ -590,15 +696,15 @@ that will block developer's from committing work if it does not get approved by
 a script.
 
 Git hook scripts need to be manually installed on a per-repository basis, so
-Grunt Drupal Tasks facilitates by allowing easy configuration of the grunt-githooks
-plugin as part of our `grunt git-setup` task.
+Grunt Drupal Tasks facilitates by allowing easy configuration of the
+grunt-githooks plugin as part of our `grunt git-setup` task.
 
 #### Default Task
 
-By default, Grunt Drupal Tasks installs a special variant of the `validate` task.
-This version, specifically invoked by `validate:staged` attempts to minimize the
-number of files scanned by checking only those that have changed since the last
-successful commit.
+By default, Grunt Drupal Tasks installs a special variant of the `validate`
+task. This version, specifically invoked by `validate:staged` attempts to
+minimize the number of files scanned by checking only those that have changed
+since the last successful commit.
 
 #### Configuring Additional Tasks
 
diff --git a/EXTEND.md b/EXTEND.md
index b542608..08c94a7 100644
--- a/EXTEND.md
+++ b/EXTEND.md
@@ -73,17 +73,58 @@ var taskList = ['phpcs:full'];
 grunt.registerTask('validate', taskList);
 ```
 
+### Customizing Help Output (Help API)
+
+If you add custom tasks to your project and want them exposed as part of the
+`help` task, you may add a simple code snippet to your Gruntfile.js or any
+loaded task file.
+
+```js
+var Help = require('grunt-drupal-tasks/lib/help');
+
+Help.addItem('existing-task', 'Named Group', 'Optional description that avoids the default task description.');
+
+Help.add({
+  task: 'existing-task',
+  group: 'Named Group',
+  description: 'Optional description that avoids the default task description.'
+});
+
+Help.add([
+  {
+    task: 'existing-task',
+    group: 'Named Group',
+    description: 'Optional description that avoids the default task description.'
+  },
+  {
+    task: 'second-task',
+    group: 'Named Group',
+    description: 'A second registered task to register with the help system.'
+  }
+]);
+```
+
+If you want to include your task in one of the existing groups, copy the text
+exactly as seen in the output of the `grunt help` task.
+
 ### Leveraging Bash Scripts
 
 If you have an existing project or a kit of useful project utilities, you may
-have some Bash scripts laying around that you want to keep. You can fold them
-into the grunt tools and worry about reimplementing them later using something
-like the simple wrapper script based on
-[grunt-shell](https://github.com/sindresorhus/grunt-shell) displayed below.
+have some Bash scripts laying around that you want to keep. Sometimes Bash
+or PHP are simply better for managing a Drupal system, other times you want to
+keep the up-front time of integrating the Grunt tools minimal.
+
+The preferred approach is to configure your scripts via the **Project
+Operations** system. This allows you to run your script via
+`grunt custom-install`, and pass any grunt-derived configuration you might need
+to the script.
+
+If you want to automatically load your scripts, here is a script based on
+[grunt-shell](https://github.com/sindresorhus/grunt-shell) you can use.
 
 ```js
 module.exports = function(grunt) {
- 
+
 /**
  * Define "bin" wrapper tasks.
  */
@@ -99,6 +140,7 @@ if (files) {
   }
 };
 ```
+
 This particular snippet requires your bash scripts end in `.sh` and be located
 in the directory `root:bin/`.
 
diff --git a/README.md b/README.md
index a89e78e..8ec2b30 100644
--- a/README.md
+++ b/README.md
@@ -77,7 +77,14 @@ These environment variables will override other options.
 
 * `GDT_DOMAIN`: Specify the base URL for live system testing. Falls back to a
 setting in the project's Gruntconfig.json, then hostname if not set.
+* `GDT_INSTALL_PROFILE`: Overrides the install profile specified in the
+project's Gruntconfig.json to be used by the `install` task.
 * `GDT_QUIET`: If evaluated truthy, will suppress all desktop notifications.
+* `GDT_SITE_ALIAS`: Configure the default Drush site alias for the project.
+* `GDT_SITEURLS`: Overrides the URL(s) for the project's site(s) specified in
+the Gruntconfig.json by which each can be accessed for end-to-end testing by
+tools such as Behat. Use instead of `GDT_DOMAIN` if the project has multiple
+subsites or URLs.
 
 ## Setting Up and Extending
 
diff --git a/bootstrap.js b/bootstrap.js
index dd6e04c..b25ad73 100644
--- a/bootstrap.js
+++ b/bootstrap.js
@@ -1,11 +1,15 @@
 module.exports = function(grunt) {
   // Initialize global configuration variables.
   var config = grunt.file.readJSON('Gruntconfig.json');
-  grunt.initConfig({
-    config: config
-  });
+  if (grunt.config.getRaw() === undefined) {
+    grunt.initConfig({
+      config: config
+    });
+  }
+  else {
+    grunt.config.set('config', config);
+  }
 
-  // Set up default global configuration.
   var GDT = require('./lib/init')(grunt);
   GDT.init();
 
diff --git a/example/Gruntconfig.json b/example/Gruntconfig.json
index 8f9c102..d687d6b 100644
--- a/example/Gruntconfig.json
+++ b/example/Gruntconfig.json
@@ -20,5 +20,8 @@
   "behat": {
     "flags": "--tags ~@wip"
   },
-  "eslint": true
+  "eslint": true,
+  "scripts": {
+    "update": "<%= config.drush.cmd %> <%= config.alias %> features-revert-all -yv"
+  }
 }
diff --git a/lib/init.js b/lib/init.js
index 73fe81e..f7c251f 100644
--- a/lib/init.js
+++ b/lib/init.js
@@ -3,9 +3,14 @@ var _ = require('lodash');
 module.exports = function(grunt) {
   var module = {};
 
-  module.domain = function() {
-    return process.env.GDT_DOMAIN || grunt.config('config.domain') || require('os').hostname();
-  };
+  // Allow projects to insert config into the gdt namespace before loading GDT bootstrap.
+  // This will take precedence over environment or project-shipped config.
+  module.magic = function (name, envName) {
+    envName = envName || name;
+    return grunt.config(['gdt', name])
+      || process.env['GDT_' + envName.toUpperCase()]
+      || grunt.config(['config', name]);
+  }
 
   // Ensure a default URL for the project.
   module.siteUrls = function() {
@@ -39,7 +44,10 @@ module.exports = function(grunt) {
     if (grunt.config('config.siteUrls') === undefined)
       grunt.config('config.siteUrls', {});
 
-    grunt.config('config.domain', module.domain());
+    grunt.config('config.domain', module.magic('domain') || require('os').hostname());
+    grunt.config('config.alias', module.magic('alias', 'SITE_ALIAS') || '@' + grunt.config('config.domain'));
+    grunt.config('config.project.profile', module.magic('profile', 'INSTALL_PROFILE') || 'standard');
+    grunt.config('config.siteUrls', module.magic('siteUrls') || {default: module.siteUrls});
     grunt.config('config.siteUrls.default', module.siteUrls());
     grunt.config('config.buildPaths', module.buildPaths());
 
diff --git a/lib/scripts.js b/lib/scripts.js
index d9240e1..b672ce2 100644
--- a/lib/scripts.js
+++ b/lib/scripts.js
@@ -1,6 +1,46 @@
 module.exports = function(grunt) {
   var module = {};
 
+  var buildCmd = function(name, cmd, execOptions) {
+    grunt.config(['shell', name + '-dispatch'], {
+      command: cmd,
+      options: {
+        gruntLogHeader: false
+      }
+    });
+
+    if (execOptions) {
+      grunt.config(['shell', name + '-dispatch', 'options', 'execOptions'], execOptions);
+    }
+
+    return 'shell:' + name + '-dispatch';
+  }
+
+  /**
+   * Generate configuration and retrieve commands for an arbitrary task name.
+   *
+   * Useful to add configuration-driven "events" to other tasks.
+   */
+  module.eventify = function(scripts, task, namespace, tasks) {
+    if (!scripts) {
+      return tasks;
+    }
+
+    var command;
+    if (scripts['pre-' + task]) {
+      command = buildCmd('pre-' + namespace, scripts['pre-' + task]);
+      tasks.unshift(command);
+
+    }
+    if (scripts['post-' + task]) {
+      command = buildCmd('post-' + namespace, scripts['post-' + task]);
+      tasks.push(command);
+    }
+
+    return tasks;
+  }
+
+  // Generate configuration and retrieve commands for a defined script operation.
   module.handle = function(config, task, namespace) {
     var command = '';
 
@@ -15,16 +55,32 @@ module.exports = function(grunt) {
     }
 
     if (config.scripts[task]) {
-      grunt.config(['shell', namespace + '-dispatch'], {
-        command: config.scripts[task],
-        options: {
-          execOptions: {
-            cwd: config.path
-          },
-          gruntLogHeader: false
+      var command = [];
+      if (config.scripts['pre-' + task]) {
+        command.push(buildCmd(
+          'pre-' + namespace,
+          config.scripts['pre-' + task],
+          {
+            cwd: config.path || process.cwd()
+          }
+        ));
+      }
+      command.push(buildCmd(
+        namespace,
+        config.scripts[task],
+        {
+          cwd: config.path || process.cwd()
         }
-      });
-      command = 'shell:' + namespace + '-dispatch';
+      ));
+      if (config.scripts['post-' + task]) {
+        command.push(buildCmd(
+          'post-' + namespace,
+          config.scripts['post-' + task],
+          {
+            cwd: config.path || process.cwd()
+          }
+        ));
+      }
     }
     else {
       grunt.log.writeln('The ' + namespace + ' command "' + task + '"" is not available. Select from the following:');
@@ -38,6 +94,7 @@ module.exports = function(grunt) {
     for (var key in scripts) {
       grunt.log.writeln(key + '\t:\t' + scripts[key]);
     }
+    grunt.log.writeln('Scripts named with "pre-<op>" or "post-<op>" are automatically run before or after their respective <op> operation.');
   }
 
   return module;
diff --git a/package.json b/package.json
index e28fe68..9c9451b 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
     "grunt-githooks": "^0.3.1",
     "grunt-eslint": "13.0.0",
     "grunt-force-task": "1.0.0",
-    "grunt-log-headers": "1.0.1",
+    "grunt-githooks": "^0.3.1",
+    "grunt-log-headers": "^1.0.1",
     "grunt-mkdir": "0.1.2",
     "grunt-newer": "1.1.1",
     "grunt-notify": "git+https://github.com/grayside/grunt-notify.git#0.5.0",
diff --git a/tasks/install.js b/tasks/install.js
index 15ba5db..e1d7b44 100644
--- a/tasks/install.js
+++ b/tasks/install.js
@@ -46,33 +46,43 @@ module.exports = function(grunt) {
   });
 
   grunt.registerTask('install', 'Install Drupal with the configured profile, or from a database dump file if one exists.', function() {
-
     var done = this.async();
 
     // Check for a database file first.
-    var dbPath = grunt.config('config.install.db');
+    var dbPath = grunt.config('config.project.db');
     if (dbPath && grunt.file.exists(dbPath)) {
       Drupal.loadDatabaseConnection(function(error) {
         if (error) {
           grunt.fail.fatal(error);
           done();
         }
-        grunt.task.run([
+
+        var tasks = [
           // Drop current database.
           'drush:sql-drop',
           // Load new database.
           'shell:loaddb',
           // Run any pending database updates.
-          'drush:updb'
-        ]);
+          'drush:updb',
+        ];
+
+        tasks = require('../lib/scripts')(grunt)
+          .eventify(grunt.config('config.scripts'), this.name, 'ops', tasks);
+
+        grunt.task.run(tasks);
         done();
       });
     }
     else {
-      grunt.task.run([
+      var tasks = [
         // Run site install.
         'drush:install'
-      ]);
+      ];
+
+      tasks = require('../lib/scripts')(grunt)
+        .eventify(grunt.config('config.scripts'), this.name, 'ops', tasks);
+
+      grunt.task.run(tasks);
       done();
     }
   });
@@ -81,6 +91,6 @@ module.exports = function(grunt) {
     {
       task: 'install',
       group: 'Operations'
-    },
+    }
   ]);
 };
diff --git a/tasks/operations.js b/tasks/operations.js
new file mode 100644
index 0000000..a8671ff
--- /dev/null
+++ b/tasks/operations.js
@@ -0,0 +1,28 @@
+module.exports = function(grunt) {
+
+  /**
+   * Define operations tasks as configured via scripts in Gruntconfig.json
+   *
+   * These are ad hoc scripts with minimal facilitation from Grunt.
+   */
+
+  if (grunt.config('config.scripts')) {
+    var scripts = grunt.config('config.scripts');
+    for (var key in scripts) {
+      grunt.registerTask(key, 'Perform the configured "' + key + '" task.', function() {
+        var task = require('../lib/scripts')(grunt)
+          .handle(grunt.config('config'), this.name, 'ops');
+
+        if (task) {
+          grunt.task.run(task);
+        }
+      });
+
+      require('../lib/help')(grunt).add({
+        task: key,
+        group: 'Operations'
+      });
+    }
+  }
+
+}


Test patch:
diff --git a/test/build.js b/test/build.js
index 161f155..8ce5635 100644
--- a/test/build.js
+++ b/test/build.js
@@ -95,14 +95,50 @@ describe('grunt', function() {
     });
   });
 
-  describe('themes configuration', function() {
-    it('should run theme scripts', function(done) {
-      exec('grunt themes:example_theme:write', function (error, stdout, stderr) {
-        var status = !error && stdout && stdout.match(/scripts\srun/)[0];
+  describe('Script dispatching', function() {
+    it('should pass commands along to themes', function(done) {
+      exec('grunt themes:example_theme:echo', function (error, stdout, stderr) {
+        var status = !error && stdout && stdout.match(/theme\sscripts\srun/)[0] && stdout.match(/run in example_theme/)[0];
         assert.ok(status);
         done();
       });
     });
+
+    it('should run project operation scripts', function(done) {
+      exec('grunt echo', function (error, stdout, stderr) {
+        var status = !error && stdout && stdout.match(/operational\sscripts\srun/)[0];
+        assert.ok(status);
+        done();
+      });
+    });
+
+    it('should run pre-op and post-op scripts', function(done) {
+      exec('grunt echo', function (error, stdout, stderr) {
+        var status = !error && stdout && stdout.match(/pre\-op script/)[0] && stdout.match(/post\-op script/)[0];
+        assert.ok(status);
+        done();
+      });
+    });
+
+    it('should include project operation scripts in help output', function(done) {
+      exec('grunt help', function (error, stdout, stderr) {
+        var status = !error && stdout && stdout.match(/Perform\sthe\sconfigured\s"echo"\stask/)[0];
+        assert.ok(status);
+        done();
+      });
+    });
+  });
+
+  describe('grunt task', function() {
+    it('"git-setup" may be run to install git hooks.', function(done) {
+      exec('grunt git-setup', function(error, stdout, stderr) {
+        fs.exists('./.git/hooks/pre-commit', function(exists) {
+          assert.ok(!error && exists);
+          done();
+        });
+      });
+    });
+
   });
 
   describe('grunt task', function() {
diff --git a/test/library.js b/test/library.js
index f1c9cef..52d7a4a 100644
--- a/test/library.js
+++ b/test/library.js
@@ -25,3 +25,60 @@ describe('Utility Functions', function() {
     });
   });
 });
+
+describe('Initialization', function() {
+  var grunt = require('grunt');
+
+  describe('of magic configuration', function() {
+    it('should defer to the "GDT" namespace', function(done) {
+      grunt.config.init({
+        config: { test_a: 'Enterprise' },
+        gdt: { test_a: 'Victory' }
+      });
+      process.env['GDT_TEST_A'] = 'Yamaguchi';
+
+      var init = require('../lib/init')(grunt);
+      assert.equal(init.magic('test_a'), 'Victory');
+      done();
+    });
+    it('should defer to environment over normal project configuration', function(done) {
+      grunt.config.init({
+        config: { test_b: 'Enterprise' }
+      });
+      process.env['GDT_TEST_B'] = 'Yamaguchi';
+
+      var init = require('../lib/init')(grunt);
+      assert.equal(init.magic('test_b'), 'Yamaguchi');
+      done();
+    });
+    it('should fall back to the project configuration', function(done) {
+      grunt.config.init({
+        config: { test_c: 'Enterprise' }
+      });
+
+      var init = require('../lib/init')(grunt);
+      assert.equal(init.magic('test_c'), 'Enterprise');
+      done();
+    });
+    it('should allow override of the environment variable name', function(done) {
+      grunt.config.init({
+        config: { test_d: 'Enterprise' }
+      });
+      process.env['GDT_TEST_DELTA'] = 'Yamaguchi';
+
+      var init = require('../lib/init')(grunt);
+      assert.equal(init.magic('test_d', 'test_delta'), 'Yamaguchi');
+      done();
+    });
+    it('should allow override of the environment variable name without affecting priority', function(done) {
+      grunt.config.init({
+        config: { test_e: 'Enterprise' }
+      });
+
+      var init = require('../lib/init')(grunt);
+      assert.equal(init.magic('test_e', 'test_epsilon'), 'Enterprise');
+      done();
+    });
+  });
+
+});
diff --git a/test/test_assets/Gruntconfig.json b/test/test_assets/Gruntconfig.json
index 94e69d8..5af78ac 100644
--- a/test/test_assets/Gruntconfig.json
+++ b/test/test_assets/Gruntconfig.json
@@ -26,12 +26,17 @@
     "ignoreError": true
   },
   "eslint": true,
+  "scripts": {
+    "pre-echo": "echo 'pre-op script'",
+    "echo": "echo 'operational scripts run'",
+    "post-echo": "echo 'post-op script'"
+  },
   "themes": {
     "example_theme": {
       "path": "<%= config.srcPaths.drupal %>/themes/example_theme",
       "compass": true,
       "scripts": {
-        "write": "echo 'scripts run'"
+        "echo": "echo 'theme scripts run in '$(basename $(pwd))"
       }
     }
   }
diff --git a/test/test_assets_d8/Gruntconfig.json b/test/test_assets_d8/Gruntconfig.json
index 94e69d8..5af78ac 100644
--- a/test/test_assets_d8/Gruntconfig.json
+++ b/test/test_assets_d8/Gruntconfig.json
@@ -26,12 +26,17 @@
     "ignoreError": true
   },
   "eslint": true,
+  "scripts": {
+    "pre-echo": "echo 'pre-op script'",
+    "echo": "echo 'operational scripts run'",
+    "post-echo": "echo 'post-op script'"
+  },
   "themes": {
     "example_theme": {
       "path": "<%= config.srcPaths.drupal %>/themes/example_theme",
       "compass": true,
       "scripts": {
-        "write": "echo 'scripts run'"
+        "echo": "echo 'theme scripts run in '$(basename $(pwd))"
       }
     }
   }