Make it easier to add tests

Make it easier to add tests

Instead of having to deal with a massive mess of rules every time you
want to add a new test, you can now just add a new .spec.ts file (as
long as it is in generic_ui/scripts or generic_core).

This change looks a lot scarier than it is, so a quick description of
what it really done:

This moves the gruntConfig object to a variable named gruntConfig

This adds a new function to the Gruntfile to look for all the spec files
in a directory and make rules to browserify those files and run them
(return the tasks to run to do that).

This uses that function for generic_ui/scripts and generic_core

That's all!  Sorry about how ugly the diff is going to look...

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -2,186 +2,12 @@
 Gruntfile for uProxy
 ###

-TaskManager = require 'uproxy-lib/build/tools/taskmanager'
-
-#-------------------------------------------------------------------------
-# Define the tasks
-taskManager = new TaskManager.Manager()
-
-taskManager.add 'base', [
-  'copy:dev'
-  'ts:devInModuleEnv'
-  'ts:generic_ui'
-  'version_file'
-  'browserify:chromeAppMain'
-  'browserify:genericCoreFreedomModule'
-  'browserify:ccaMain'
-]
-
-#
-taskManager.add 'version_file', [
-  'gitinfo'
-  'string-replace:version'
-]
-
-taskManager.add 'build_chrome_app', [
-  'base'
-  'ts:chrome_app'
-  'copy:chrome_app'
-  'vulcanize:chromeAppInline'
-  'vulcanize:chromeAppCsp'
-  'copy:chrome_app'
-]
-
-taskManager.add 'build_chrome_ext', [
-  'base'
-  'ts:chrome_extension'
-  'copy:chrome_extension'
-  'copy:chrome_extension_additional'
-  'vulcanize:chromeExtInline'
-  'vulcanize:chromeExtCsp'
-  'vulcanize:chromeViewLogsInline'
-  'vulcanize:chromeViewLogsCsp'
-  'browserify:chromeExtMain'
-  'browserify:chromeContext'
-  'browserify:chromeVulcanized'
-  'browserify:chromeLogsVulcanized'
-  'string-replace:chromeExtVulcanized'
-  'string-replace:chromeExtLogsVulcanized'
-]
-
-taskManager.add 'build_chrome', [
-  'build_chrome_app'
-  'build_chrome_ext'
-]
-
-# Firefox build tasks.
-taskManager.add 'build_firefox', [
-  'base'
-  'ts:firefox'
-  'copy:firefox'
-  'copy:firefox_additional'
-  'vulcanize:firefoxInline'
-  'vulcanize:firefoxCsp'
-  'vulcanize:firefoxViewLogsInline'
-  'vulcanize:firefoxViewLogsCsp'
-  'string-replace:firefoxVulcanized'
-  'string-replace:firefoxLogsVulcanized'
-  'browserify:firefoxContext'
-  'browserify:firefoxVulcanized'
-  'browserify:firefoxLogsVulcanized'
-]
-
-# CCA build tasks.
-taskManager.add 'build_cca', [
-  'base'
-  'ts:cca'
-  'copy:cca'
-  'copy:cca_additional'
-  'vulcanize:ccaInline'
-  'vulcanize:ccaCsp'
-  'browserify:ccaMain'
-  'browserify:ccaContext'
-  'browserify:ccaVulcanized'
-  'browserify:ccaLogsVulcanized'
-  'string-replace:ccaVulcanized'
-  'string-replace:ccaLogsVulcanized'
-]
-
-# Mobile OS build tasks
-taskManager.add 'build_android', [
-  'exec:rmAndroidBuild'
-  'build_cca'
-  'exec:ccaCreate'
-  'exec:ccaBuildAndroid'
-]
-
-# Emulate the mobile client
-taskManager.add 'emulate_android', [
- 'build_android'
- 'exec:ccaEmulateAndroid'
-]
-
-taskManager.add 'build_ios', [
-  'exec:rmIosBuild'
-  'build_cca'
-  'exec:ccaCreateIos'
-  'exec:ccaBuildIos'
-]
-
-# --- Testing tasks ---
-taskManager.add 'test_core', [
-  'base'
-  'browserify:genericCoreFirewallSpec'
-  'browserify:genericCoreUproxyCoreSpec'
-  'browserify:genericCoreLocalInstanceSpec'
-  'browserify:genericCoreMetricsSpec'
-  'browserify:genericCoreRemoteInstanceSpec'
-  'browserify:genericCoreRemoteConnectionSpec'
-  'browserify:genericCoreRemoteUserSpec'
-  'browserify:genericCoreSocialSpec'
-  'browserify:genericCoreStorageSpec'
-  'jasmine:generic_core'
-]
-
-taskManager.add 'test_ui', [
-  'base'
-  'browserify:genericUiUiSpec'
-  'browserify:genericUiUserSpec'
-  'browserify:genericUiCopypasteStateSpec'
-  'jasmine:generic_ui'
-]
-
-taskManager.add 'test_chrome', [
-  'build_chrome'
-  'browserify:chromeExtensionCoreConnectorSpec'
-  'jasmine:chrome_extension'
-]
-
-taskManager.add 'integration_test', [
-  'build_chrome'
-  'copy:integration'
-  'ts:integration_specs'
-  'ts:integration_freedom_module'
-  'browserify:integrationSpec'
-  'browserify:integrationFreedomModule'
-  'jasmine_chromeapp'
-]
-
-taskManager.add 'everything', [
-  'build'
-  'test'
-  'integration_test'
-]
-
-# This is the target run by Travis. Targets in here should run locally
-# and on Travis/Sauce Labs.
-taskManager.add 'test', [
-  'test_core'
-  'test_ui'
-  'test_chrome'
-]
-
-taskManager.add 'build', [
-  'build_chrome'
-  'build_firefox'
-  'build_cca'
-]
-
-taskManager.add 'dist', [
-  'build'
-  'copy:dist'
-  'mozilla-addon-sdk'
-  'mozilla-cfx-xpi:dist'
-]
-
-taskManager.add 'default', [
-  'build'
-]
-
-#-------------------------------------------------------------------------
+fs = require('fs')
 rules = require './build/tools/common-grunt-rules'
 path = require 'path'
+TaskManager = require 'uproxy-lib/build/tools/taskmanager'
+
+#-------------------------------------------------------------------------

 # Location of where src is copied into and compiled.
 devBuildPath = 'build/dev/uproxy'
@@ -221,16 +47,6 @@
 ccaPath = path.dirname(require.resolve('cca/package.json'))
 freedomForChromePath = path.dirname(require.resolve('freedom-for-chrome/package.json'))
 uproxyLibPath = path.dirname(require.resolve('uproxy-lib/package.json'))
-
-#ipaddrjsPath = path.dirname(require.resolve('ipaddr.js/package.json'))
-# TODO(ldixon): update utransformers package to uproxy-obfuscators
-# uproxyObfuscatorsPath = path.dirname(require.resolve('uproxy-obfuscators/package.json'))
-# uproxyObfuscatorsPath = path.dirname(require.resolve('utransformers/package.json'))
-# regex2dfaPath = path.dirname(require.resolve('regex2dfa/package.json'))
-# Cordova testing
-# ccaPath = path.dirname(require.resolve('cca/app/package.json'))
-# pgpPath = path.dirname(require.resolve('freedom-pgp-e2e/package.json'))
-

 #-------------------------------------------------------------------------
 # TODO: Move more file lists here.
@@ -320,813 +136,995 @@
     module: 'commonjs'
     fast: 'always'

-module.exports = (grunt) ->
-  grunt.initConfig {
-    pkg: grunt.file.readJSON('package.json')
-    pkgs:
-      lib: grunt.file.readJSON('node_modules/uproxy-lib/package.json')
-      freedom: grunt.file.readJSON('node_modules/freedom/package.json')
-      freedomchrome: grunt.file.readJSON('node_modules/freedom-for-chrome/package.json')
-      freedomfirefox: grunt.file.readJSON('node_modules/freedom-for-firefox/package.json')
-      freedomxmpp: grunt.file.readJSON('node_modules/freedom-social-xmpp/package.json')
-      freedomfirebase: grunt.file.readJSON('node_modules/freedom-social-firebase/package.json')
-      freedomwechat: grunt.file.readJSON('node_modules/freedom-social-wechat/package.json')
-
-    clean: ['build/dev', 'build/dist', '.tscache']
-
-    #-------------------------------------------------------------------------
-    # Import global names into config name space
-    ccaJsPath: path.join(ccaPath, 'src/cca.js')
-    androidDevPath: androidDevPath
-    ccaDevPath: ccaDevPath
-    iosDevPath: iosDevPath
-    exec: {
-      ccaCreate: {
-        command: '<%= ccaJsPath %> create <%= androidDevPath %> --link-to=<%= ccaDevPath %>'
-      }
-      ccaBuildAndroid: {
-        cwd: '<%= androidDevPath %>'
-        command: '<%= ccaJsPath %> build android'
-      }
-      ccaEmulateAndroid: {
-        cwd: '<%= androidDevPath %>'
-        command: '<%= ccaJsPath %> run android --emulator'
-      }
-      rmAndroidBuild: {
-        command: 'rm -rf <%= androidDevPath %>'
-      }
-      ccaCreateIos: {
-        command: '<%= ccaJsPath %> create <%= iosDevPath %> --link-to=<%= ccaDevPath %>'
-      }
-      ccaBuildIos: {
-        cwd: '<%= iosDevPath %>'
-        command: '<%= ccaJsPath %> build --webview=system ios'
-      }
-      rmIosBuild: {
-        command: 'rm -rf <%= iosDevPath %>'
+gruntConfig = {
+  pkg: JSON.parse(fs.readFileSync('package.json', 'utf8'))
+  pkgs:
+    lib: JSON.parse(fs.readFileSync('node_modules/uproxy-lib/package.json', 'utf8'))
+    freedom: JSON.parse(fs.readFileSync('node_modules/freedom/package.json', 'utf8'))
+    freedomchrome: JSON.parse(fs.readFileSync('node_modules/freedom-for-chrome/package.json', 'utf8'))
+    freedomfirefox: JSON.parse(fs.readFileSync('node_modules/freedom-for-firefox/package.json', 'utf8'))
+    freedomxmpp: JSON.parse(fs.readFileSync('node_modules/freedom-social-xmpp/package.json', 'utf8'))
+    freedomfirebase: JSON.parse(fs.readFileSync('node_modules/freedom-social-firebase/package.json', 'utf8'))
+    freedomwechat: JSON.parse(fs.readFileSync('node_modules/freedom-social-wechat/package.json', 'utf8'))
+
+  clean: ['build/dev', 'build/dist', '.tscache']
+
+  #-------------------------------------------------------------------------
+  # Import global names into config name space
+  ccaJsPath: path.join(ccaPath, 'src/cca.js')
+  androidDevPath: androidDevPath
+  ccaDevPath: ccaDevPath
+  iosDevPath: iosDevPath
+  exec: {
+    ccaCreate: {
+      command: '<%= ccaJsPath %> create <%= androidDevPath %> --link-to=<%= ccaDevPath %>'
+    }
+    ccaBuildAndroid: {
+      cwd: '<%= androidDevPath %>'
+      command: '<%= ccaJsPath %> build android'
+    }
+    ccaEmulateAndroid: {
+      cwd: '<%= androidDevPath %>'
+      command: '<%= ccaJsPath %> run android --emulator'
+    }
+    rmAndroidBuild: {
+      command: 'rm -rf <%= androidDevPath %>'
+    }
+    ccaCreateIos: {
+      command: '<%= ccaJsPath %> create <%= iosDevPath %> --link-to=<%= ccaDevPath %>'
+    }
+    ccaBuildIos: {
+      cwd: '<%= iosDevPath %>'
+      command: '<%= ccaJsPath %> build --webview=system ios'
+    }
+    rmIosBuild: {
+      command: 'rm -rf <%= iosDevPath %>'
+    }
+  }
+
+  copy: {
+    # Copy all needed third party libraries to appropriate locations.
+    thirdParty:
+      files: [
+        # Copy local |third_party| files into dev: so that the third_party
+        # dependencies are always in the common |build/third_party| location.
+        # This allows path to reference typescript definitions for ambient
+        # contexts to always be found, even in generated `.d.ts` files..
+        {
+            nonull: true,
+            expand: true,
+            cwd: 'third_party'
+            src: ['**/*'],
+            dest: thirdPartyBuildPath,
+        }
+        # Copy distribution directory of uproxy-lib so all paths can always
+        # find their dependencies. Note that this also requires uproxy-lib
+        # references to find those in |build/third_party/|. These paths
+        # are delicate.
+        {
+            nonull: true,
+            expand: true,
+            cwd: path.join(uproxyLibPath, 'build/dist'),
+            src: ['**/*'],
+            dest: path.join(thirdPartyBuildPath, 'uproxy-lib/'),
+        },
+        # Use the third_party definitions from uproxy-lib. Copied to the same
+        # location relative to their compiled location in uproxy-lib so they
+        # have the same relative path to the created `.d.ts` files from
+        # |build/dev|.
+        {
+            nonull: true,
+            expand: true,
+            cwd: path.join(uproxyLibPath, 'third_party'),
+            src: ['**/*'],
+            dest: thirdPartyBuildPath
+        }
+      ]
+
+    # Copy releveant non-typescript src files to dev build.
+    dev:
+      files: [
+        {
+            nonull: true,
+            expand: true,
+            cwd: 'src/',
+            src: [
+              '**/*',
+              '!generic_core/dist_build/*',
+              '!generic_core/dev_build/*'
+            ],
+            dest: devBuildPath,
+            onlyIf: 'modified'
+        }
+        {
+            nonull: true,
+            expand: true,
+            cwd: 'src/generic_core/dev_build/',
+            src: ['*'],
+            dest: devBuildPath + '/generic_core',
+            onlyIf: 'modified'
+        }
+      ]
+
+    # Copy releveant files for distribution.
+    dist:
+      files: [
+        { # Chrome extension
+          expand: true
+          cwd: chromeExtDevPath
+          src: [
+            'manifest.json'
+
+            'bower/webcomponentsjs/webcomponents.min.js'
+            'bower/polymer/polymer.js'
+
+            'generic_ui/*.html'
+            'generic_ui/polymer/vulcanized*.{html,js}'
+            '!generic_ui/polymer/vulcanized*inline.html'
+            '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+
+            'generic_ui/scripts/copypaste.js'
+            'generic_ui/scripts/get_logs.js'
+            'scripts/context.static.js'
+            'scripts/background.static.js'
+            '!**/*spec*'
+
+            'generic_ui/style/*.css'
+
+            # extra components we use
+            'generic_ui/fonts/*'
+            'generic_ui/icons/*'
+            'icons/*'
+            '_locales/**'
+          ]
+          dest: 'build/dist/chrome/extension'
+        }
+        { # Chrome app
+          expand: true
+          cwd: chromeAppDevPath
+          src: [
+            'manifest.json'
+            '*.html'
+
+            'bower/webcomponentsjs/webcomponents.min.js'
+            'bower/polymer/polymer.js'
+
+            # UI for not-connected
+            # This is not browserified so we use .js instead of .static.js
+            'polymer/vulcanized.{html,js}'
+
+            # actual scripts that run things
+            'freedomjs-anonymized-metrics/anonmetrics.json'
+            'freedomjs-anonymized-metrics/metric.js'
+            'freedom-for-chrome/freedom-for-chrome.js'
+            'freedom-social-xmpp/social.google.json'
+            'freedom-social-xmpp/socialprovider.js'
+            'freedom-social-xmpp/vcardstore.js'
+            'freedom-social-xmpp/node-xmpp-browser.js'
+            'freedom-social-xmpp/google-auth.js'
+            'freedom-social-firebase/social.firebase-facebook.json'
+            'freedom-social-firebase/firebase-shims.js'
+            'freedom-social-firebase/firebase.js'
+            'freedom-social-firebase/firebase-social-provider.js'
+            'freedom-social-firebase/facebook-social-provider.js'
+            'freedom-social-wechat/social.wechat.json'
+            'freedom-port-control/port-control.js'
+            'freedom-port-control/port-control.json'
+            'freedom-pgp-e2e/end-to-end.compiled.js'
+            'freedom-pgp-e2e/googstorage.js'
+            'freedom-pgp-e2e/e2e.js'
+            'freedom-pgp-e2e/pgpapi.json'
+
+            '**/freedom-module.json'
+            '!generic_core/freedom-module.json'
+            '**/*.static.js'
+            '!**/*spec*'
+
+            'icons/*'
+            'fonts/*'
+            '_locales/**'
+          ]
+          dest: 'build/dist/chrome/app'
+        }
+        { # Chrome app freedom-module
+          expand: true
+          cwd: 'src/generic_core/dist_build/'
+          src: ['*']
+          dest: 'build/dist/chrome/app/generic_core'
+        }
+        { # Firefox
+          expand: true
+          cwd: firefoxDevPath
+          src: [
+            'package.json'
+
+            # addon sdk scripts
+            'lib/**/*.js'
+
+            'data/freedomjs-anonymized-metrics/anonmetrics.json'
+            'data/freedomjs-anonymized-metrics/metric.js'
+            'data/freedom-for-firefox/freedom-for-firefox.jsm'
+            'data/freedom-social-xmpp/social.google.json'
+            'data/freedom-social-xmpp/socialprovider.js'
+            'data/freedom-social-xmpp/vcardstore.js'
+            'data/freedom-social-xmpp/node-xmpp-browser.js'
+            'data/freedom-social-xmpp/google-auth.js'
+            'data/freedom-social-firebase/social.firebase-facebook.json'
+            'data/freedom-social-firebase/firebase-shims.js'
+            'data/freedom-social-firebase/firebase.js'
+            'data/freedom-social-firebase/firebase-social-provider.js'
+            'data/freedom-social-firebase/facebook-social-provider.js'
+            'data/freedom-social-wechat/social.wechat.json'
+            'data/freedom-port-control/port-control.js'
+            'data/freedom-port-control/port-control.json'
+            'data/freedom-pgp-e2e/end-to-end.compiled.js'
+            'data/freedom-pgp-e2e/googstorage.js'
+            'data/freedom-pgp-e2e/e2e.js'
+            'data/freedom-pgp-e2e/pgpapi.json'
+
+            'data/**/freedom-module.json'
+            '!generic_core/freedom-module.json'
+            'data/**/*.static.js'
+            'data/generic_ui/scripts/get_logs.js'
+            'data/scripts/content-proxy.js'
+            '!**/*spec*'
+
+            'data/bower/webcomponentsjs/webcomponents.min.js'
+            'data/bower/polymer/polymer.js'
+
+            'data/generic_ui/*.html'
+            'data/generic_ui/polymer/vulcanized*.{html,js}'
+            '!data/generic_ui/polymer/vulcanized*inline.html'
+            '!data/generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+
+            'data/generic_ui/style/*.css'
+
+            'data/fonts/*'
+            'data/icons/*'
+            'data/generic_ui/fonts/*'
+            'data/generic_ui/icons/*'
+          ]
+          dest: 'build/dist/firefox'
+        }
+        { # Firefox freedom-module
+          expand: true
+          cwd: 'src/generic_core/dist_build/'
+          src: ['*']
+          dest: 'build/dist/firefox/data/generic_core/'
+        }
+        { # CCA app
+          expand: true
+          cwd: ccaDevPath
+          src: [
+            'manifest.json'
+            '*.html'
+
+            'bower/webcomponentsjs/webcomponents.min.js'
+            'bower/polymer/polymer.js'
+
+            'generic_ui/*.html'
+            'generic_ui/polymer/vulcanized*.{html,js}'
+            '!generic_ui/polymer/vulcanized*inline.html'
+            '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
+
+            'generic_ui/scripts/copypaste.js'
+            'generic_ui/scripts/get_logs.js'
+            '!**/*spec*'
+
+            'generic_ui/style/*.css'
+
+            # extra components we use
+            'generic_ui/fonts/*'
+            'generic_ui/icons/*'
+
+            # This is not browserified so we use .js instead of .static.js
+            'polymer/vulcanized.{html,js}'
+
+            # actual scripts that run things
+            'freedomjs-anonymized-metrics/anonmetrics.json'
+            'freedomjs-anonymized-metrics/metric.js'
+            'freedom-for-chrome/freedom-for-chrome.js'
+            'freedom-social-xmpp/social.google.json'
+            'freedom-social-xmpp/socialprovider.js'
+            'freedom-social-xmpp/vcardstore.js'
+            'freedom-social-xmpp/node-xmpp-browser.js'
+            'freedom-social-xmpp/google-auth.js'
+            'freedom-social-firebase/social.firebase-facebook.json'
+            'freedom-social-firebase/firebase-shims.js'
+            'freedom-social-firebase/firebase.js'
+            'freedom-social-firebase/firebase-social-provider.js'
+            'freedom-social-firebase/facebook-social-provider.js'
+            'freedom-social-wechat/social.wechat.json'
+            'freedom-port-control/port-control.js'
+            'freedom-port-control/port-control.json'
+
+            '**/freedom-module.json'
+            '!generic_core/freedom-module.json'
+            '**/*.static.js'
+
+            'icons/*'
+            'fonts/*'
+          ]
+          dest: 'build/dist/cca'
+        }
+        { # Chrome app freedom-module
+          expand: true
+          cwd: 'src/generic_core/dist_build/'
+          src: ['*']
+          dest: 'build/dist/cca/app/generic_core'
+        }
+      ]
+
+    chrome_extension:
+      Rule.copyLibs
+        npmLibNames: [
+        ]
+        pathsFromDevBuild: [
+          'generic_ui'
+          'interfaces'
+          'icons'
+          'fonts'
+        ]
+        pathsFromThirdPartyBuild: [
+          'bower'
+        ]
+        files: [
+          {
+            expand: true, cwd: devBuildPath, flatten: true
+            src: FILES.uproxy_common
+            dest: chromeExtDevPath + '/generic_ui/scripts'
+          }
+          {
+            expand: true, cwd: devBuildPath, flatten: true
+            src: FILES.uproxy_common
+            dest: chromeExtDevPath + '/scripts'
+          }
+          {
+            expand: true, cwd: devBuildPath, flatten: true
+            src: FILES.uproxy_common
+            dest: chromeExtDevPath + '/generic'
+          }
+        ]
+        localDestPath: 'chrome/extension'
+    chrome_extension_additional:
+      files: [
+        { # copy chrome extension panel components from the background
+          expand: true, cwd: chromeExtDevPath
+          src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*', '*.html']
+          dest: chromeExtDevPath + '/generic_ui'
+        }
+      ]
+
+    chrome_app:
+      Rule.copyLibs
+        npmLibNames: [
+          'freedom-for-chrome'
+        ]
+        pathsFromDevBuild: [
+          'generic_core'
+        ]
+        pathsFromThirdPartyBuild: [
+          'bower'
+          'sha1'
+          'uproxy-lib/loggingprovider'
+          'uproxy-lib/churn-pipe'
+        ]
+        files: [
+          {
+            expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
+            src: ['anonmetrics.json', 'metric.js']
+            dest: chromeAppDevPath + '/freedomjs-anonymized-metrics'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
+            src: ['**']
+            dest: chromeAppDevPath + '/freedom-social-xmpp'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
+            src: ['**']
+            dest: chromeAppDevPath + '/freedom-social-firebase'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
+            src: ['**']
+            dest: chromeAppDevPath + '/freedom-social-wechat'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-pgp-e2e/dist/',
+            src: ['**']
+            dest: chromeAppDevPath + '/freedom-pgp-e2e'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-port-control/dist/',
+            src: ['**']
+            dest: chromeAppDevPath + '/freedom-port-control'
+          },
+          { # uProxy Icons and fonts
+            expand: true, cwd: 'src/'
+            src: ['icons/128_online.png', 'fonts/*']
+            dest: chromeAppDevPath
+          }
+        ]
+        localDestPath: 'chrome/app/'
+
+    # {
+    #   # Copy third party UI files required for polymer.
+    #   expand: true, cwd: 'third_party/lib'
+    #   src: FILES.thirdPartyUi
+    #   dest: chromeAppDevPath + 'lib'
+    # }, {
+    #   # Chrome app polymer.
+    #   # (Assumes vulcanize tasks have executed)
+    #   expand: true, cwd: 'build/compile-src/chrome/app/polymer'
+    #   src: ['vulcanized.*']
+    #   dest: chromeAppDevPath + 'polymer'
+    # }]
+
+    # Firefox. Assumes the top-level tasks generic_core and generic_ui
+    # completed.
+    firefox:
+      Rule.copyLibs
+        npmLibNames: [
+          'freedom-for-firefox'
+        ]
+        pathsFromDevBuild: [
+          'generic_core'
+          'generic_ui'
+          'interfaces'
+          'icons'
+          'fonts'
+        ]
+        pathsFromThirdPartyBuild: [
+          'bower'
+          'sha1'
+          'uproxy-lib/loggingprovider'
+          'uproxy-lib/churn-pipe'
+        ]
+        files: [
+          {
+            expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
+            src: ['anonmetrics.json', 'metric.js']
+            dest: firefoxDevPath + 'data/freedomjs-anonymized-metrics'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
+            src: ['**']
+            dest: firefoxDevPath + '/data/freedom-social-xmpp'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
+            src: ['**']
+            dest: firefoxDevPath + '/data/freedom-social-firebase'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
+            src: ['**']
+            dest: firefoxDevPath + '/data/freedom-social-wechat'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-pgp-e2e/dist/',
+            src: ['**']
+            dest: firefoxDevPath + '/data/freedom-pgp-e2e'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-port-control/dist/',
+            src: ['**']
+            dest: firefoxDevPath + '/data/freedom-port-control'
+          },
+          { # lib
+            expand: true, cwd: devBuildPath
+            src: ['interfaces/*.js']
+            dest: firefoxDevPath + '/lib'
+          }
+        ]
+        localDestPath: 'firefox/data'
+    firefox_additional:
+      files: [
+        { # copy chrome extension panel components from the background
+          expand: true, cwd: firefoxDevPath + '/data'
+          src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*']
+          dest: firefoxDevPath + '/data/generic_ui'
+        }
+        { # copy generic files used by core and UI
+          expand: true, cwd: genericPath
+          src: ['*.js']
+          dest: firefoxDevPath + '/data/generic'
+        }
+      ]
+    cca:
+      Rule.copyLibs
+        npmLibNames: [
+          'freedom-for-chrome'
+        ]
+        pathsFromDevBuild: [
+          'generic_core'
+          'generic_ui'
+          'interfaces'
+          'icons'
+          'fonts'
+        ]
+        pathsFromThirdPartyBuild: [
+          'bower'
+          'sha1'
+          'uproxy-lib/loggingprovider'
+          'uproxy-lib/churn-pipe'
+        ]
+        files: [
+          {
+            expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
+            src: ['anonmetrics.json', 'metric.js']
+            dest: ccaDevPath + '/freedomjs-anonymized-metrics'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
+            src: ['**']
+            dest: ccaDevPath + '/freedom-social-xmpp'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
+            src: ['**']
+            dest: ccaDevPath + '/freedom-social-firebase'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
+            src: ['**']
+            dest: ccaDevPath + '/freedom-social-wechat'
+          },
+          {
+            expand: true, cwd: 'node_modules/freedom-port-control/dist/',
+            src: ['**']
+            dest: ccaDevPath + '/freedom-port-control'
+          },
+          { # uProxy Icons and fonts
+            expand: true, cwd: 'src/'
+            src: ['icons/128_online.png', 'fonts/*']
+            dest: ccaDevPath
+          }
+        ]
+        localDestPath: 'cca/app/'
+    cca_additional:
+      files: [
+        { # copy chrome extension panel components from the background
+          expand: true, cwd: ccaDevPath
+          src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*', '*.html']
+          dest: ccaDevPath + '/generic_ui'
+        }
+        { # copy generic files used by core and UI
+          expand: true, cwd: genericPath
+          src: ['*.js']
+          dest: ccaDevPath + '/generic'
+        }
+      ]
+
+
+
+    integration:
+      files: [ {
+        # Copy compiled Chrome App code, required for integration tests
+        expand: true, cwd: chromeAppDevPath
+        src: ['**', '!**/spec', '!**/*.md', '!**/*.ts']
+        dest: devBuildPath + '/integration'
+      }]
+  }  # copy
+
+  #-------------------------------------------------------------------------
+  'string-replace':
+    version:
+      files: [
+        {
+          src: path.join(devBuildPath, 'generic/version.js')
+          dest: path.join(devBuildPath, 'generic/version.js')
+        }
+      ]
+      options:
+        replacements: [{
+          pattern: /\"___VERSION_TEMPLATE___\"/g
+          replacement: JSON.stringify
+            version: '<%= pkg.version %>'
+            gitcommit: '<%= gitinfo.local.branch.current.SHA %>'
+            'uproxy-lib': '<%= pkgs.lib.version %>'
+            freedom: '<%= pkgs.freedom.version %>'
+            'freedom-for-chrome': '<%= pkgs.freedomchrome.version %>'
+            'freedom-for-firefox': '<%= pkgs.freedomfirefox.version %>'
+            'freedom-social-xmpp': '<%= pkgs.freedomxmpp.version %>'
+            'freedom-social-firebase': '<%= pkgs.freedomfirebase.version %>'
+            'freedom-social-wechat': '<%= pkgs.freedomwechat.version %>'
+        }]
+    chromeExtVulcanized:
+      finishVulcanized(chromeExtDevPath + '/generic_ui', 'vulcanized')
+
+    firefoxVulcanized:
+      finishVulcanized(firefoxDevPath + '/data/generic_ui', 'vulcanized')
+
+    chromeExtLogsVulcanized:
+      finishVulcanized(chromeExtDevPath + '/generic_ui', 'vulcanized-view-logs')
+
+    firefoxLogsVulcanized:
+      finishVulcanized(firefoxDevPath + '/data/generic_ui', 'vulcanized-view-logs')
+
+    ccaVulcanized:
+      finishVulcanized(ccaDevPath + '/generic_ui', 'vulcanized')
+
+    ccaLogsVulcanized:
+      finishVulcanized(ccaDevPath + '/generic_ui', 'vulcanized-view-logs')
+  #-------------------------------------------------------------------------
+  # All typescript compiles to locations in `build/`
+  # Typescript compilation rules
+  ts:
+    # Compile all non-sample typescript code into the development build
+    # directory.
+    devInModuleEnv: compileTypescript [
+      devBuildPath + '/interfaces/**/*.ts'
+      devBuildPath + '/generic_core/**/*.ts'
+      '!' + devBuildPath + '/**/*.core-env.ts'
+      '!' + devBuildPath + '/**/*.core-env.spec.ts'
+    ]
+
+    generic_ui: compileTypescript [
+      devBuildPath + '/generic_ui/**/*.ts'
+      devBuildPath + '/**/*.core-env.spec.ts'
+      devBuildPath + '/**/*.core-env.ts'
+    ]
+
+    chrome_extension: compileTypescript [
+      devBuildPath + '/chrome/extension/**/*.ts'
+    ]
+
+    chrome_app: compileTypescript [
+      devBuildPath + '/chrome/app/**/*.ts'
+    ]
+
+    firefox: compileTypescript [
+      devBuildPath + '/firefox/**/*.ts'
+    ]
+
+    cca: compileTypescript [
+      devBuildPath + '/cca/**/*.ts'
+    ]
+
+    integration_specs: compileTypescript [
+      devBuildPath + '/integration/*.ts'
+      '!' + devBuildPath + '/integration/test_connection.ts'
+    ]
+    integration_freedom_module: compileTypescript [
+      devBuildPath + '/integration/test_connection.ts'
+    ]
+
+
+  browserify:
+    chromeAppMain: Rule.browserify 'chrome/app/scripts/main.core-env'
+    chromeExtMain: Rule.browserify('chrome/extension/scripts/background',
+      browserifyOptions:
+        standalone: 'ui_context'
+    )
+    chromeContext: Rule.browserify('chrome/extension/scripts/context',
+      browserifyOptions:
+        standalone: 'ui_context'
+    )
+
+    chromeVulcanized: Rule.browserify('chrome/extension/generic_ui/polymer/vulcanized', {})# no exports from this
+    chromeLogsVulcanized: Rule.browserify('chrome/extension/generic_ui/polymer/vulcanized-view-logs', {})
+    firefoxContext:
+      src: [
+        firefoxDevPath + '/data/scripts/background.js'
+      ]
+      dest: firefoxDevPath + '/data/scripts/context.static.js'
+      options:
+        browserifyOptions:
+          standalone: 'ui_context'
+    firefoxVulcanized: Rule.browserify('firefox/data/generic_ui/polymer/vulcanized', {})# no exports from this
+    firefoxLogsVulcanized: Rule.browserify('firefox/data/generic_ui/polymer/vulcanized-view-logs', {})
+
+    ccaMain: Rule.browserify('cca/app/scripts/main.core-env',
+      browserifyOptions:
+        standalone: 'ui_context'
+    )
+    ccaContext: Rule.browserify('cca/app/scripts/context',
+      browserifyOptions:
+        standalone: 'ui_context'
+    )
+    ccaVulcanized: Rule.browserify('cca/app/generic_ui/polymer/vulcanized', {})# no exports from this
+    ccaLogsVulcanized: Rule.browserify('cca/app/generic_ui/polymer/vulcanized-view-logs', {})
+
+    chromeExtensionCoreConnector: Rule.browserify 'chrome/extension/scripts/chrome_core_connector'
+    chromeExtensionCoreConnectorSpec: Rule.browserifySpec 'chrome/extension/scripts/chrome_core_connector'
+    genericCoreFirewall: Rule.browserify 'generic_core/firewall'
+#    genericCoreFirewallSpec: Rule.browserifySpec 'generic_core/firewall'
+    genericCoreFreedomModule: Rule.browserify 'generic_core/freedom-module'
+#    genericCoreUproxyCoreSpec: Rule.browserifySpec 'generic_core/uproxy_core'
+#    genericCoreLocalInstanceSpec: Rule.browserifySpec 'generic_core/local-instance'
+#    genericCoreMetricsSpec: Rule.browserifySpec 'generic_core/metrics'
+#    genericCoreRemoteConnectionSpec: Rule.browserifySpec 'generic_core/remote-connection'
+#    genericCoreRemoteInstanceSpec: Rule.browserifySpec 'generic_core/remote-instance'
+#    genericCoreRemoteUserSpec: Rule.browserifySpec 'generic_core/remote-user'
+#    genericCoreSocialSpec: Rule.browserifySpec 'generic_core/social'
+#    genericCoreStorageSpec: Rule.browserifySpec 'generic_core/storage'
+
+#    genericUiUiSpec: Rule.browserifySpec 'generic_ui/scripts/ui'
+#    genericUiUserSpec: Rule.browserifySpec 'generic_ui/scripts/user'
+    genericUiCopypasteStateSpec: Rule.browserifySpec 'generic_ui/scripts/copypaste-state'
+    integrationSpec: Rule.browserifySpec 'integration/core'
+    integrationFreedomModule: Rule.browserify 'integration/test_connection'
+
+  #-------------------------------------------------------------------------
+  jasmine:
+    chrome_extension: Rule.jasmineSpec('chrome/extension/scripts/',
+        [path.join('build/dev/uproxy/mocks/chrome_mocks.js')])
+
+  jasmine_chromeapp: {
+    all: {
+      files: [
+        {
+          cwd: devBuildPath + '/integration/',
+          src: ['**/*'],
+          dest: './',
+          expand: true
+        }
+      ],
+      scripts: ['freedom-for-chrome/freedom-for-chrome.js',
+                'core.spec.static.js'
+      ],
+      options: {
+        outdir: 'build/dev/uproxy/integration/'
+        # Uncomment this for debugging
+        # keepRunner: true,
       }
     }
-
-    copy: {
-      # Copy all needed third party libraries to appropriate locations.
-      thirdParty:
-        files: [
-          # Copy local |third_party| files into dev: so that the third_party
-          # dependencies are always in the common |build/third_party| location.
-          # This allows path to reference typescript definitions for ambient
-          # contexts to always be found, even in generated `.d.ts` files..
-          {
-              nonull: true,
-              expand: true,
-              cwd: 'third_party'
-              src: ['**/*'],
-              dest: thirdPartyBuildPath,
-          }
-          # Copy distribution directory of uproxy-lib so all paths can always
-          # find their dependencies. Note that this also requires uproxy-lib
-          # references to find those in |build/third_party/|. These paths
-          # are delicate.
-          {
-              nonull: true,
-              expand: true,
-              cwd: path.join(uproxyLibPath, 'build/dist'),
-              src: ['**/*'],
-              dest: path.join(thirdPartyBuildPath, 'uproxy-lib/'),
-          },
-          # Use the third_party definitions from uproxy-lib. Copied to the same
-          # location relative to their compiled location in uproxy-lib so they
-          # have the same relative path to the created `.d.ts` files from
-          # |build/dev|.
-          {
-              nonull: true,
-              expand: true,
-              cwd: path.join(uproxyLibPath, 'third_party'),
-              src: ['**/*'],
-              dest: thirdPartyBuildPath
-          }
-        ]
-
-      # Copy releveant non-typescript src files to dev build.
-      dev:
-        files: [
-          {
-              nonull: true,
-              expand: true,
-              cwd: 'src/',
-              src: [
-                '**/*',
-                '!generic_core/dist_build/*',
-                '!generic_core/dev_build/*'
-              ],
-              dest: devBuildPath,
-              onlyIf: 'modified'
-          }
-          {
-              nonull: true,
-              expand: true,
-              cwd: 'src/generic_core/dev_build/',
-              src: ['*'],
-              dest: devBuildPath + '/generic_core',
-              onlyIf: 'modified'
-          }
-        ]
-
-      # Copy releveant files for distribution.
-      dist:
-        files: [
-          { # Chrome extension
-            expand: true
-            cwd: chromeExtDevPath
-            src: [
-              'manifest.json'
-
-              'bower/webcomponentsjs/webcomponents.min.js'
-              'bower/polymer/polymer.js'
-
-              'generic_ui/*.html'
-              'generic_ui/polymer/vulcanized*.{html,js}'
-              '!generic_ui/polymer/vulcanized*inline.html'
-              '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
-
-              'generic_ui/scripts/copypaste.js'
-              'generic_ui/scripts/get_logs.js'
-              'scripts/context.static.js'
-              'scripts/background.static.js'
-              '!**/*spec*'
-
-              'generic_ui/style/*.css'
-
-              # extra components we use
-              'generic_ui/fonts/*'
-              'generic_ui/icons/*'
-              'icons/*'
-              '_locales/**'
-            ]
-            dest: 'build/dist/chrome/extension'
-          }
-          { # Chrome app
-            expand: true
-            cwd: chromeAppDevPath
-            src: [
-              'manifest.json'
-              '*.html'
-
-              'bower/webcomponentsjs/webcomponents.min.js'
-              'bower/polymer/polymer.js'
-
-              # UI for not-connected
-              # This is not browserified so we use .js instead of .static.js
-              'polymer/vulcanized.{html,js}'
-
-              # actual scripts that run things
-              'freedomjs-anonymized-metrics/anonmetrics.json'
-              'freedomjs-anonymized-metrics/metric.js'
-              'freedom-for-chrome/freedom-for-chrome.js'
-              'freedom-social-xmpp/social.google.json'
-              'freedom-social-xmpp/socialprovider.js'
-              'freedom-social-xmpp/vcardstore.js'
-              'freedom-social-xmpp/node-xmpp-browser.js'
-              'freedom-social-xmpp/google-auth.js'
-              'freedom-social-firebase/social.firebase-facebook.json'
-              'freedom-social-firebase/firebase-shims.js'
-              'freedom-social-firebase/firebase.js'
-              'freedom-social-firebase/firebase-social-provider.js'
-              'freedom-social-firebase/facebook-social-provider.js'
-              'freedom-social-wechat/social.wechat.json'
-              'freedom-port-control/port-control.js'
-              'freedom-port-control/port-control.json'
-              'freedom-pgp-e2e/end-to-end.compiled.js'
-              'freedom-pgp-e2e/googstorage.js'
-              'freedom-pgp-e2e/e2e.js'
-              'freedom-pgp-e2e/pgpapi.json'
-
-              '**/freedom-module.json'
-              '!generic_core/freedom-module.json'
-              '**/*.static.js'
-              '!**/*spec*'
-
-              'icons/*'
-              'fonts/*'
-              '_locales/**'
-            ]
-            dest: 'build/dist/chrome/app'
-          }
-          { # Chrome app freedom-module
-            expand: true
-            cwd: 'src/generic_core/dist_build/'
-            src: ['*']
-            dest: 'build/dist/chrome/app/generic_core'
-          }
-          { # Firefox
-            expand: true
-            cwd: firefoxDevPath
-            src: [
-              'package.json'
-
-              # addon sdk scripts
-              'lib/**/*.js'
-
-              'data/freedomjs-anonymized-metrics/anonmetrics.json'
-              'data/freedomjs-anonymized-metrics/metric.js'
-              'data/freedom-for-firefox/freedom-for-firefox.jsm'
-              'data/freedom-social-xmpp/social.google.json'
-              'data/freedom-social-xmpp/socialprovider.js'
-              'data/freedom-social-xmpp/vcardstore.js'
-              'data/freedom-social-xmpp/node-xmpp-browser.js'
-              'data/freedom-social-xmpp/google-auth.js'
-              'data/freedom-social-firebase/social.firebase-facebook.json'
-              'data/freedom-social-firebase/firebase-shims.js'
-              'data/freedom-social-firebase/firebase.js'
-              'data/freedom-social-firebase/firebase-social-provider.js'
-              'data/freedom-social-firebase/facebook-social-provider.js'
-              'data/freedom-social-wechat/social.wechat.json'
-              'data/freedom-port-control/port-control.js'
-              'data/freedom-port-control/port-control.json'
-              'data/freedom-pgp-e2e/end-to-end.compiled.js'
-              'data/freedom-pgp-e2e/googstorage.js'
-              'data/freedom-pgp-e2e/e2e.js'
-              'data/freedom-pgp-e2e/pgpapi.json'
-
-              'data/**/freedom-module.json'
-              '!generic_core/freedom-module.json'
-              'data/**/*.static.js'
-              'data/generic_ui/scripts/get_logs.js'
-              'data/scripts/content-proxy.js'
-              '!**/*spec*'
-
-              'data/bower/webcomponentsjs/webcomponents.min.js'
-              'data/bower/polymer/polymer.js'
-
-              'data/generic_ui/*.html'
-              'data/generic_ui/polymer/vulcanized*.{html,js}'
-              '!data/generic_ui/polymer/vulcanized*inline.html'
-              '!data/generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
-
-              'data/generic_ui/style/*.css'
-
-              'data/fonts/*'
-              'data/icons/*'
-              'data/generic_ui/fonts/*'
-              'data/generic_ui/icons/*'
-            ]
-            dest: 'build/dist/firefox'
-          }
-          { # Firefox freedom-module
-            expand: true
-            cwd: 'src/generic_core/dist_build/'
-            src: ['*']
-            dest: 'build/dist/firefox/data/generic_core/'
-          }
-          { # CCA app
-            expand: true
-            cwd: ccaDevPath
-            src: [
-              'manifest.json'
-              '*.html'
-
-              'bower/webcomponentsjs/webcomponents.min.js'
-              'bower/polymer/polymer.js'
-
-              'generic_ui/*.html'
-              'generic_ui/polymer/vulcanized*.{html,js}'
-              '!generic_ui/polymer/vulcanized*inline.html'
-              '!generic_ui/polymer/vulcanized.js' # vulcanized.html uses vulcanized.static.js
-
-              'generic_ui/scripts/copypaste.js'
-              'generic_ui/scripts/get_logs.js'
-              '!**/*spec*'
-
-              'generic_ui/style/*.css'
-
-              # extra components we use
-              'generic_ui/fonts/*'
-              'generic_ui/icons/*'
-
-              # This is not browserified so we use .js instead of .static.js
-              'polymer/vulcanized.{html,js}'
-
-              # actual scripts that run things
-              'freedomjs-anonymized-metrics/anonmetrics.json'
-              'freedomjs-anonymized-metrics/metric.js'
-              'freedom-for-chrome/freedom-for-chrome.js'
-              'freedom-social-xmpp/social.google.json'
-              'freedom-social-xmpp/socialprovider.js'
-              'freedom-social-xmpp/vcardstore.js'
-              'freedom-social-xmpp/node-xmpp-browser.js'
-              'freedom-social-xmpp/google-auth.js'
-              'freedom-social-firebase/social.firebase-facebook.json'
-              'freedom-social-firebase/firebase-shims.js'
-              'freedom-social-firebase/firebase.js'
-              'freedom-social-firebase/firebase-social-provider.js'
-              'freedom-social-firebase/facebook-social-provider.js'
-              'freedom-social-wechat/social.wechat.json'
-              'freedom-port-control/port-control.js'
-              'freedom-port-control/port-control.json'
-
-              '**/freedom-module.json'
-              '!generic_core/freedom-module.json'
-              '**/*.static.js'
-
-              'icons/*'
-              'fonts/*'
-            ]
-            dest: 'build/dist/cca'
-          }
-          { # Chrome app freedom-module
-            expand: true
-            cwd: 'src/generic_core/dist_build/'
-            src: ['*']
-            dest: 'build/dist/cca/app/generic_core'
-          }
-        ]
-
-      chrome_extension:
-        Rule.copyLibs
-          npmLibNames: [
-          ]
-          pathsFromDevBuild: [
-            'generic_ui'
-            'interfaces'
-            'icons'
-            'fonts'
-          ]
-          pathsFromThirdPartyBuild: [
-            'bower'
-          ]
-          files: [
-            {
-              expand: true, cwd: devBuildPath, flatten: true
-              src: FILES.uproxy_common
-              dest: chromeExtDevPath + '/generic_ui/scripts'
-            }
-            {
-              expand: true, cwd: devBuildPath, flatten: true
-              src: FILES.uproxy_common
-              dest: chromeExtDevPath + '/scripts'
-            }
-            {
-              expand: true, cwd: devBuildPath, flatten: true
-              src: FILES.uproxy_common
-              dest: chromeExtDevPath + '/generic'
-            }
-          ]
-          localDestPath: 'chrome/extension'
-      chrome_extension_additional:
-        files: [
-          { # copy chrome extension panel components from the background
-            expand: true, cwd: chromeExtDevPath
-            src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*', '*.html']
-            dest: chromeExtDevPath + '/generic_ui'
-          }
-        ]
-
-      chrome_app:
-        Rule.copyLibs
-          npmLibNames: [
-            'freedom-for-chrome'
-          ]
-          pathsFromDevBuild: [
-            'generic_core'
-          ]
-          pathsFromThirdPartyBuild: [
-            'bower'
-            'sha1'
-            'uproxy-lib/loggingprovider'
-            'uproxy-lib/churn-pipe'
-          ]
-          files: [
-            {
-              expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
-              src: ['anonmetrics.json', 'metric.js']
-              dest: chromeAppDevPath + '/freedomjs-anonymized-metrics'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
-              src: ['**']
-              dest: chromeAppDevPath + '/freedom-social-xmpp'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
-              src: ['**']
-              dest: chromeAppDevPath + '/freedom-social-firebase'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
-              src: ['**']
-              dest: chromeAppDevPath + '/freedom-social-wechat'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-pgp-e2e/dist/',
-              src: ['**']
-              dest: chromeAppDevPath + '/freedom-pgp-e2e'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-port-control/dist/',
-              src: ['**']
-              dest: chromeAppDevPath + '/freedom-port-control'
-            },
-            { # uProxy Icons and fonts
-              expand: true, cwd: 'src/'
-              src: ['icons/128_online.png', 'fonts/*']
-              dest: chromeAppDevPath
-            }
-          ]
-          localDestPath: 'chrome/app/'
-
-      # {
-      #   # Copy third party UI files required for polymer.
-      #   expand: true, cwd: 'third_party/lib'
-      #   src: FILES.thirdPartyUi
-      #   dest: chromeAppDevPath + 'lib'
-      # }, {
-      #   # Chrome app polymer.
-      #   # (Assumes vulcanize tasks have executed)
-      #   expand: true, cwd: 'build/compile-src/chrome/app/polymer'
-      #   src: ['vulcanized.*']
-      #   dest: chromeAppDevPath + 'polymer'
-      # }]
-
-      # Firefox. Assumes the top-level tasks generic_core and generic_ui
-      # completed.
-      firefox:
-        Rule.copyLibs
-          npmLibNames: [
-            'freedom-for-firefox'
-          ]
-          pathsFromDevBuild: [
-            'generic_core'
-            'generic_ui'
-            'interfaces'
-            'icons'
-            'fonts'
-          ]
-          pathsFromThirdPartyBuild: [
-            'bower'
-            'sha1'
-            'uproxy-lib/loggingprovider'
-            'uproxy-lib/churn-pipe'
-          ]
-          files: [
-            {
-              expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
-              src: ['anonmetrics.json', 'metric.js']
-              dest: firefoxDevPath + 'data/freedomjs-anonymized-metrics'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
-              src: ['**']
-              dest: firefoxDevPath + '/data/freedom-social-xmpp'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
-              src: ['**']
-              dest: firefoxDevPath + '/data/freedom-social-firebase'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
-              src: ['**']
-              dest: firefoxDevPath + '/data/freedom-social-wechat'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-pgp-e2e/dist/',
-              src: ['**']
-              dest: firefoxDevPath + '/data/freedom-pgp-e2e'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-port-control/dist/',
-              src: ['**']
-              dest: firefoxDevPath + '/data/freedom-port-control'
-            },
-            { # lib
-              expand: true, cwd: devBuildPath
-              src: ['interfaces/*.js']
-              dest: firefoxDevPath + '/lib'
-            }
-          ]
-          localDestPath: 'firefox/data'
-      firefox_additional:
-        files: [
-          { # copy chrome extension panel components from the background
-            expand: true, cwd: firefoxDevPath + '/data'
-            src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*']
-            dest: firefoxDevPath + '/data/generic_ui'
-          }
-          { # copy generic files used by core and UI
-            expand: true, cwd: genericPath
-            src: ['*.js']
-            dest: firefoxDevPath + '/data/generic'
-          }
-        ]
-      cca:
-        Rule.copyLibs
-          npmLibNames: [
-            'freedom-for-chrome'
-          ]
-          pathsFromDevBuild: [
-            'generic_core'
-            'generic_ui'
-            'interfaces'
-            'icons'
-            'fonts'
-          ]
-          pathsFromThirdPartyBuild: [
-            'bower'
-            'sha1'
-            'uproxy-lib/loggingprovider'
-            'uproxy-lib/churn-pipe'
-          ]
-          files: [
-            {
-              expand: true, cwd: 'node_modules/freedomjs-anonymized-metrics/',
-              src: ['anonmetrics.json', 'metric.js']
-              dest: ccaDevPath + '/freedomjs-anonymized-metrics'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-xmpp/dist/',
-              src: ['**']
-              dest: ccaDevPath + '/freedom-social-xmpp'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-firebase/dist/',
-              src: ['**']
-              dest: ccaDevPath + '/freedom-social-firebase'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-social-wechat/dist/',
-              src: ['**']
-              dest: ccaDevPath + '/freedom-social-wechat'
-            },
-            {
-              expand: true, cwd: 'node_modules/freedom-port-control/dist/',
-              src: ['**']
-              dest: ccaDevPath + '/freedom-port-control'
-            },
-            { # uProxy Icons and fonts
-              expand: true, cwd: 'src/'
-              src: ['icons/128_online.png', 'fonts/*']
-              dest: ccaDevPath
-            }
-          ]
-          localDestPath: 'cca/app/'
-      cca_additional:
-        files: [
-          { # copy chrome extension panel components from the background
-            expand: true, cwd: ccaDevPath
-            src: ['polymer/*', 'scripts/*', 'icons/*', 'fonts/*', '*.html']
-            dest: ccaDevPath + '/generic_ui'
-          }
-          { # copy generic files used by core and UI
-            expand: true, cwd: genericPath
-            src: ['*.js']
-            dest: ccaDevPath + '/generic'
-          }
-        ]
-
-
-
-      integration:
-        files: [ {
-          # Copy compiled Chrome App code, required for integration tests
-          expand: true, cwd: chromeAppDevPath
-          src: ['**', '!**/spec', '!**/*.md', '!**/*.ts']
-          dest: devBuildPath + '/integration'
-        }]
-    }  # copy
-
-    #-------------------------------------------------------------------------
-    'string-replace':
-      version:
-        files: [
-          {
-            src: path.join(devBuildPath, 'generic/version.js')
-            dest: path.join(devBuildPath, 'generic/version.js')
-          }
-        ]
-        options:
-          replacements: [{
-            pattern: /\"___VERSION_TEMPLATE___\"/g
-            replacement: JSON.stringify
-              version: '<%= pkg.version %>'
-              gitcommit: '<%= gitinfo.local.branch.current.SHA %>'
-              'uproxy-lib': '<%= pkgs.lib.version %>'
-              freedom: '<%= pkgs.freedom.version %>'
-              'freedom-for-chrome': '<%= pkgs.freedomchrome.version %>'
-              'freedom-for-firefox': '<%= pkgs.freedomfirefox.version %>'
-              'freedom-social-xmpp': '<%= pkgs.freedomxmpp.version %>'
-              'freedom-social-firebase': '<%= pkgs.freedomfirebase.version %>'
-              'freedom-social-wechat': '<%= pkgs.freedomwechat.version %>'
-          }]
-      chromeExtVulcanized:
-        finishVulcanized(chromeExtDevPath + '/generic_ui', 'vulcanized')
-
-      firefoxVulcanized:
-        finishVulcanized(firefoxDevPath + '/data/generic_ui', 'vulcanized')
-
-      chromeExtLogsVulcanized:
-        finishVulcanized(chromeExtDevPath + '/generic_ui', 'vulcanized-view-logs')
-
-      firefoxLogsVulcanized:
-        finishVulcanized(firefoxDevPath + '/data/generic_ui', 'vulcanized-view-logs')
-
-      ccaVulcanized:
-        finishVulcanized(ccaDevPath + '/generic_ui', 'vulcanized')
-
-      ccaLogsVulcanized:
-        finishVulcanized(ccaDevPath + '/generic_ui', 'vulcanized-view-logs')
-    #-------------------------------------------------------------------------
-    # All typescript compiles to locations in `build/`
-    # Typescript compilation rules
-    ts:
-      # Compile all non-sample typescript code into the development build
-      # directory.
-      devInModuleEnv: compileTypescript [
-        devBuildPath + '/interfaces/**/*.ts'
-        devBuildPath + '/generic_core/**/*.ts'
-        '!' + devBuildPath + '/**/*.core-env.ts'
-        '!' + devBuildPath + '/**/*.core-env.spec.ts'
-      ]
-
-      generic_ui: compileTypescript [
-        devBuildPath + '/generic_ui/**/*.ts'
-        devBuildPath + '/**/*.core-env.spec.ts'
-        devBuildPath + '/**/*.core-env.ts'
-      ]
-
-      chrome_extension: compileTypescript [
-        devBuildPath + '/chrome/extension/**/*.ts'
-      ]
-
-      chrome_app: compileTypescript [
-        devBuildPath + '/chrome/app/**/*.ts'
-      ]
-
-      firefox: compileTypescript [
-        devBuildPath + '/firefox/**/*.ts'
-      ]
-
-      cca: compileTypescript [
-        devBuildPath + '/cca/**/*.ts'
-      ]
-
-      integration_specs: compileTypescript [
-      	devBuildPath + '/integration/*.ts'
-      	'!' + devBuildPath + '/integration/test_connection.ts'
-      ]
-      integration_freedom_module: compileTypescript [
-      	devBuildPath + '/integration/test_connection.ts'
-      ]
-
-
-    browserify:
-      chromeAppMain: Rule.browserify 'chrome/app/scripts/main.core-env'
-      chromeExtMain: Rule.browserify('chrome/extension/scripts/background',
-        browserifyOptions:
-          standalone: 'ui_context'
-      )
-      chromeContext: Rule.browserify('chrome/extension/scripts/context',
-        browserifyOptions:
-          standalone: 'ui_context'
-      )
-
-      chromeVulcanized: Rule.browserify('chrome/extension/generic_ui/polymer/vulcanized', {})# no exports from this
-      chromeLogsVulcanized: Rule.browserify('chrome/extension/generic_ui/polymer/vulcanized-view-logs', {})
-      firefoxContext:
-        src: [
-          firefoxDevPath + '/data/scripts/background.js'
-        ]
-        dest: firefoxDevPath + '/data/scripts/context.static.js'
-        options:
-          browserifyOptions:
-            standalone: 'ui_context'
-      firefoxVulcanized: Rule.browserify('firefox/data/generic_ui/polymer/vulcanized', {})# no exports from this
-      firefoxLogsVulcanized: Rule.browserify('firefox/data/generic_ui/polymer/vulcanized-view-logs', {})
-
-      ccaMain: Rule.browserify('cca/app/scripts/main.core-env',
-        browserifyOptions:
-          standalone: 'ui_context'
-      )
-      ccaContext: Rule.browserify('cca/app/scripts/context',
-        browserifyOptions:
-          standalone: 'ui_context'
-      )
-      ccaVulcanized: Rule.browserify('cca/app/generic_ui/polymer/vulcanized', {})# no exports from this
-      ccaLogsVulcanized: Rule.browserify('cca/app/generic_ui/polymer/vulcanized-view-logs', {})
-
-      chromeExtensionCoreConnector: Rule.browserify 'chrome/extension/scripts/chrome_core_connector'
-      chromeExtensionCoreConnectorSpec: Rule.browserifySpec 'chrome/extension/scripts/chrome_core_connector'
-      genericCoreFirewall: Rule.browserify 'generic_core/firewall'
-      genericCoreFirewallSpec: Rule.browserifySpec 'generic_core/firewall'
-      genericCoreFreedomModule: Rule.browserify 'generic_core/freedom-module'
-      genericCoreUproxyCoreSpec: Rule.browserifySpec 'generic_core/uproxy_core'
-      genericCoreLocalInstanceSpec: Rule.browserifySpec 'generic_core/local-instance'
-      genericCoreMetricsSpec: Rule.browserifySpec 'generic_core/metrics'
-      genericCoreRemoteConnectionSpec: Rule.browserifySpec 'generic_core/remote-connection'
-      genericCoreRemoteInstanceSpec: Rule.browserifySpec 'generic_core/remote-instance'
-      genericCoreRemoteUserSpec: Rule.browserifySpec 'generic_core/remote-user'
-      genericCoreSocialSpec: Rule.browserifySpec 'generic_core/social'
-      genericCoreStorageSpec: Rule.browserifySpec 'generic_core/storage'
-
-      genericUiUiSpec: Rule.browserifySpec 'generic_ui/scripts/ui'
-      genericUiUserSpec: Rule.browserifySpec 'generic_ui/scripts/user'
-      genericUiCopypasteStateSpec: Rule.browserifySpec 'generic_ui/scripts/copypaste-state'
-      integrationSpec: Rule.browserifySpec 'integration/core'
-      integrationFreedomModule: Rule.browserify 'integration/test_connection'
-
-    #-------------------------------------------------------------------------
-    jasmine:
-      chrome_extension: Rule.jasmineSpec('chrome/extension/scripts/',
-          [path.join('build/dev/uproxy/mocks/chrome_mocks.js')]);
-      generic_core: Rule.jasmineSpec 'generic_core'
-      generic_ui: Rule.jasmineSpec 'generic_ui/scripts'
-
-
-    jasmine_chromeapp: {
-      all: {
-        files: [
-          {
-            cwd: devBuildPath + '/integration/',
-            src: ['**/*'],
-            dest: './',
-            expand: true
-          }
-        ],
-        scripts: ['freedom-for-chrome/freedom-for-chrome.js',
-                  'core.spec.static.js'
-        ],
-        options: {
-          outdir: 'build/dev/uproxy/integration/'
-          # Uncomment this for debugging
-          # keepRunner: true,
-        }
-      }
-    }
-    'mozilla-addon-sdk':
-      'latest':
-        options:
-          dest_dir: '.mozilla_addon_sdk/'
-
-    'mozilla-cfx-xpi':
-      'dist':
-        options:
-          'mozilla-addon-sdk': 'latest'
-          extension_dir: 'build/dist/firefox'
-          dist_dir: 'build/dist'
-
-    vulcanize:
-      chromeExtInline:
-        vulcanizeInline(
-            chromeExtDevPath + '/generic_ui/polymer/root.html',
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized-inline.html')
-      chromeExtCsp:
-        vulcanizeCsp(
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized-inline.html',
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized.html')
-      chromeAppInline:
-        vulcanizeInline(
-            chromeAppDevPath + '/polymer/ext-missing.html',
-            chromeAppDevPath + '/polymer/vulcanized-inline.html')
-      chromeAppCsp:
-        vulcanizeCsp(
-            chromeAppDevPath + '/polymer/vulcanized-inline.html',
-            chromeAppDevPath + '/polymer/vulcanized.html')
-      firefoxInline:
-        vulcanizeInline(
-            firefoxDevPath + '/data/generic_ui/polymer/root.html',
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized-inline.html')
-      firefoxCsp:
-        vulcanizeCsp(
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized-inline.html',
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized.html')
-      ccaInline:
-        vulcanizeInline(
-            ccaDevPath + '/generic_ui/polymer/root.html',
-            ccaDevPath + '/generic_ui/polymer/vulcanized-inline.html',
-            ccaDevPath + '/polymer/ext-missing.html',
-            ccaDevPath + '/polymer/vulcanized-inline.html')
-      ccaCsp:
-        vulcanizeCsp(
-            ccaDevPath + '/generic_ui/polymer/vulcanized-inline.html',
-            ccaDevPath + '/generic_ui/polymer/vulcanized.html',
-            ccaDevPath + '/polymer/vulcanized-inline.html',
-            ccaDevPath + '/polymer/vulcanized.html')
-      chromeViewLogsInline:
-        vulcanizeInline(
-            chromeExtDevPath + '/generic_ui/polymer/logs.html',
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs-inline.html')
-      chromeViewLogsCsp:
-        vulcanizeCsp(
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs-inline.html',
-            chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs.html')
-      firefoxViewLogsInline:
-        vulcanizeInline(
-            firefoxDevPath + '/data/generic_ui/polymer/logs.html',
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs-inline.html')
-      firefoxViewLogsCsp:
-        vulcanizeCsp(
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs-inline.html',
-            firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs.html')
-  }  # grunt.initConfig
-
+  }
+  'mozilla-addon-sdk':
+    'latest':
+      options:
+        dest_dir: '.mozilla_addon_sdk/'
+
+  'mozilla-cfx-xpi':
+    'dist':
+      options:
+        'mozilla-addon-sdk': 'latest'
+        extension_dir: 'build/dist/firefox'
+        dist_dir: 'build/dist'
+
+  vulcanize:
+    chromeExtInline:
+      vulcanizeInline(
+          chromeExtDevPath + '/generic_ui/polymer/root.html',
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized-inline.html')
+    chromeExtCsp:
+      vulcanizeCsp(
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized-inline.html',
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized.html')
+    chromeAppInline:
+      vulcanizeInline(
+          chromeAppDevPath + '/polymer/ext-missing.html',
+          chromeAppDevPath + '/polymer/vulcanized-inline.html')
+    chromeAppCsp:
+      vulcanizeCsp(
+          chromeAppDevPath + '/polymer/vulcanized-inline.html',
+          chromeAppDevPath + '/polymer/vulcanized.html')
+    firefoxInline:
+      vulcanizeInline(
+          firefoxDevPath + '/data/generic_ui/polymer/root.html',
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized-inline.html')
+    firefoxCsp:
+      vulcanizeCsp(
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized-inline.html',
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized.html')
+    ccaInline:
+      vulcanizeInline(
+          ccaDevPath + '/generic_ui/polymer/root.html',
+          ccaDevPath + '/generic_ui/polymer/vulcanized-inline.html',
+          ccaDevPath + '/polymer/ext-missing.html',
+          ccaDevPath + '/polymer/vulcanized-inline.html')
+    ccaCsp:
+      vulcanizeCsp(
+          ccaDevPath + '/generic_ui/polymer/vulcanized-inline.html',
+          ccaDevPath + '/generic_ui/polymer/vulcanized.html',
+          ccaDevPath + '/polymer/vulcanized-inline.html',
+          ccaDevPath + '/polymer/vulcanized.html')
+    chromeViewLogsInline:
+      vulcanizeInline(
+          chromeExtDevPath + '/generic_ui/polymer/logs.html',
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs-inline.html')
+    chromeViewLogsCsp:
+      vulcanizeCsp(
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs-inline.html',
+          chromeExtDevPath + '/generic_ui/polymer/vulcanized-view-logs.html')
+    firefoxViewLogsInline:
+      vulcanizeInline(
+          firefoxDevPath + '/data/generic_ui/polymer/logs.html',
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs-inline.html')
+    firefoxViewLogsCsp:
+      vulcanizeCsp(
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs-inline.html',
+          firefoxDevPath + '/data/generic_ui/polymer/vulcanized-view-logs.html')
+}  # grunt.initConfig
+
+#-------------------------------------------------------------------------
+# Helper functions for different components
+
+# Adds rules to the gruntConfig to build all the tests in a given directory and
+# to run those tests, returns an array of task names to be run for testing that
+# directory
+#
+# All test files should have a name ending with .spec.ts
+testDirectory = (dir) ->
+  # every test sequence should include building everything
+  testNames = ['base']
+
+  files = fs.readdirSync(path.join('src', dir))
+  for file in files
+    match = /(.+)\.spec\.ts/.exec(file)
+    if (match)
+      loc = path.join(dir, match[1])
+      testName = loc + 'spec'
+
+      gruntConfig['browserify'][testName] = Rule.browserifySpec(loc)
+      testNames.push('browserify:' + testName)
+
+  gruntConfig['jasmine'][dir] = Rule.jasmineSpec(dir)
+  testNames.push('jasmine:' + dir)
+
+  return testNames
+
+#-------------------------------------------------------------------------
+# Define the tasks
+taskManager = new TaskManager.Manager()
+
+taskManager.add 'base', [
+  'copy:dev'
+  'ts:devInModuleEnv'
+  'ts:generic_ui'
+  'version_file'
+  'browserify:chromeAppMain'
+  'browserify:genericCoreFreedomModule'
+  'browserify:ccaMain'
+]
+
+#
+taskManager.add 'version_file', [
+  'gitinfo'
+  'string-replace:version'
+]
+
+taskManager.add 'build_chrome_app', [
+  'base'
+  'ts:chrome_app'
+  'copy:chrome_app'
+  'vulcanize:chromeAppInline'
+  'vulcanize:chromeAppCsp'
+  'copy:chrome_app'
+]
+
+taskManager.add 'build_chrome_ext', [
+  'base'
+  'ts:chrome_extension'
+  'copy:chrome_extension'
+  'copy:chrome_extension_additional'
+  'vulcanize:chromeExtInline'
+  'vulcanize:chromeExtCsp'
+  'vulcanize:chromeViewLogsInline'
+  'vulcanize:chromeViewLogsCsp'
+  'browserify:chromeExtMain'
+  'browserify:chromeContext'
+  'browserify:chromeVulcanized'
+  'browserify:chromeLogsVulcanized'
+  'string-replace:chromeExtVulcanized'
+  'string-replace:chromeExtLogsVulcanized'
+]
+
+taskManager.add 'build_chrome', [
+  'build_chrome_app'
+  'build_chrome_ext'
+]
+
+# Firefox build tasks.
+taskManager.add 'build_firefox', [
+  'base'
+  'ts:firefox'
+  'copy:firefox'
+  'copy:firefox_additional'
+  'vulcanize:firefoxInline'
+  'vulcanize:firefoxCsp'
+  'vulcanize:firefoxViewLogsInline'
+  'vulcanize:firefoxViewLogsCsp'
+  'string-replace:firefoxVulcanized'
+  'string-replace:firefoxLogsVulcanized'
+  'browserify:firefoxContext'
+  'browserify:firefoxVulcanized'
+  'browserify:firefoxLogsVulcanized'
+]
+
+# CCA build tasks.
+taskManager.add 'build_cca', [
+  'base'
+  'ts:cca'
+  'copy:cca'
+  'copy:cca_additional'
+  'vulcanize:ccaInline'
+  'vulcanize:ccaCsp'
+  'browserify:ccaMain'
+  'browserify:ccaContext'
+  'browserify:ccaVulcanized'
+  'browserify:ccaLogsVulcanized'
+  'string-replace:ccaVulcanized'
+  'string-replace:ccaLogsVulcanized'
+]
+
+# Mobile OS build tasks
+taskManager.add 'build_android', [
+  'exec:rmAndroidBuild'
+  'build_cca'
+  'exec:ccaCreate'
+  'exec:ccaBuildAndroid'
+]
+
+# Emulate the mobile client
+taskManager.add 'emulate_android', [
+ 'build_android'
+ 'exec:ccaEmulateAndroid'
+]
+
+taskManager.add 'build_ios', [
+  'exec:rmIosBuild'
+  'build_cca'
+  'exec:ccaCreateIos'
+  'exec:ccaBuildIos'
+]
+
+# --- Testing tasks ---
+taskManager.add 'test_core', testDirectory('generic_core')
+
+taskManager.add 'test_ui', testDirectory('generic_ui/scripts')
+
+taskManager.add 'test_chrome', [
+  'build_chrome'
+  'browserify:chromeExtensionCoreConnectorSpec'
+  'jasmine:chrome_extension'
+]
+
+taskManager.add 'integration_test', [
+  'build_chrome'
+  'copy:integration'
+  'ts:integration_specs'
+  'ts:integration_freedom_module'
+  'browserify:integrationSpec'
+  'browserify:integrationFreedomModule'
+  'jasmine_chromeapp'
+]
+
+taskManager.add 'everything', [
+  'build'
+  'test'
+  'integration_test'
+]
+
+# This is the target run by Travis. Targets in here should run locally
+# and on Travis/Sauce Labs.
+taskManager.add 'test', [
+  'test_core'
+  'test_ui'
+  'test_chrome'
+]
+
+taskManager.add 'build', [
+  'build_chrome'
+  'build_firefox'
+  'build_cca'
+]
+
+taskManager.add 'dist', [
+  'build'
+  'copy:dist'
+  'mozilla-addon-sdk'
+  'mozilla-cfx-xpi:dist'
+]
+
+taskManager.add 'default', [
+  'build'
+]
+
+module.exports = (grunt) ->
+  grunt.initConfig(gruntConfig)
   #-------------------------------------------------------------------------
   grunt.loadNpmTasks 'grunt-browserify'
   grunt.loadNpmTasks 'grunt-contrib-clean'


Allow live reloading for dev watch task

Allow live reloading for dev watch task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -44,6 +44,8 @@
 				files: ['src/**/*.{js,coffee}', 'test/**/*.{js,coffee}']
 				tasks: ['lint', 'test', 'build', 'build-web']

+				options: livereload: yes
+
 			test:
 				files: ['src/**/*.{js,coffee}', 'test/**/*.{js,coffee}']
 				tasks: ['test']


remove better require

remove better require

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,5 +1,9 @@
-require('better-require') 'json yaml'
 fs  = require 'fs'
+yml = require 'js-yaml'
+
+require.extensions['.yml'] = (module, filename)->
+  module.exports = yml.safeLoad fs.readFileSync filename
+
 config = require './config/config.yml'

 module.exports = (grunt)->


Gruntfile: Use http server by default.

Gruntfile: Use http server by default.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -302,8 +302,8 @@
       'coffee2closure'
       'deps'
       'karma:headless'
-      # 'http-server:debug'
-      'php'
+      'http-server:debug'
+      # 'php'
       'open:dev'
       'open:coverage'
       'watch'


do not clean este-library node_modules and do not compile Gruntfile.coffee

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -7,6 +7,7 @@

   coffeeScripts = [
     'bower_components/este-library/**/*.coffee'
+    '!bower_components/este-library/Gruntfile.coffee'
     'client/app/js/**/*.coffee'
     'server/**/*.coffee'
   ]
@@ -38,6 +39,7 @@
           force: true
         src: [
           'bower_components/este-library/**/*.js'
+          '!bower_components/este-library/node_modules/**/*.js'
           'bower_components/este-library/**/*.css'
           'client/**/js/**/*.js'
           'client/**/css/**/*.css'


Change default reporter back to dots.

Change default reporter back to dots.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -183,13 +183,13 @@
         colors: true
         # test results reporter to use
         # possible values: dots || progress || growl
-        reporters: ["spec"]
+        reporters: ["dots"]
         # If browser does not capture in given timeout [ms], kill it
         captureTimeout: 5000

       unit:
         configFile: "<%= appConfig.test %>/karma.conf.coffee"
-        reporters: ["spec", "coverage"]
+        reporters: ["dots", "coverage"]
         preprocessors:
           "scripts/**/*.js": "coverage"
         coverageReporter:


Fix type in Gruntfile task name

Fix type in Gruntfile task name

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -29,7 +29,7 @@
     nodeunit:
       tests: ['test/*_test.js']

-    sassFormt:
+    sassFormat:
       files:[]
   )


Specify where to push

Specify where to push

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -88,6 +88,7 @@
         tagName: 'v%VERSION%'
         tagMessage: 'Version %VERSION%'
         push: true
+        pushTo: 'origin'

     dom_munger:
       release:


Remove auto version bump when releasing

Remove auto version bump when releasing

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -192,7 +192,6 @@
   ])

   grunt.registerTask('release', [
-    'bump',
     'pages',
     'default',
     'copy:release'


Main: avoid throwing for superadmins

Main: avoid throwing for superadmins

--- client/Core/maincontroller.coffee
+++ client/Core/maincontroller.coffee
@@ -98,7 +98,8 @@
     role = 'moderator' if 'moderator' in roles
     role = 'admin'     if 'admin'     in roles

-    return yes  if name in disabledFeatures[role]
+    return no   if !disabledFeatures[role]
+    return yes  if disabledFeatures[role] and name in disabledFeatures[role]

     return no


pricing: add utility methods for plans view

--- client/Pricing/views/plans.coffee
+++ client/Pricing/views/plans.coffee
@@ -25,6 +25,44 @@
   switchTo: (planInterval) ->

     plan.setPlanInterval(planInterval)  for _, plan of @planViews
+    @state.planInterval = planInterval
+    @refresh()
+
+
+  setState: (state) ->
+
+    KD.utils.extend @state, state
+
+    @refresh()
+
+
+  setPromotedPlan: (promotedPlan) ->
+
+    for title, planView of @planViews
+
+      planView.enable()
+
+      if title is promotedPlan
+      then planView.setClass 'promoted'
+      else planView.unsetClass 'promoted'
+
+
+  refresh: ->
+
+    { planInterval, currentPlanInterval,
+      planTitle, currentPlan } = @state
+
+    isSameInterval    = planInterval is currentPlanInterval
+    isCurrentPlanFree = currentPlan is 'free'
+
+    if isCurrentPlanFree
+      @setPromotedPlan @state.promotedPlan
+      @planViews['free'].disable()
+    else if isSameInterval
+      @setPromotedPlan null
+      @planViews[currentPlan].disable()
+    else
+      @setPromotedPlan @state.promotedPlan

   plans: [


Link on SSH view

Link on SSH view

--- client/code/view/view/tile.coffee
+++ client/code/view/view/tile.coffee
@@ -8,6 +8,8 @@
     'click .hide-view': 'hideView'
     'click .dropdown-menu a': 'dropdownMenuItemClick'
     'click .rename-view': 'renameViewClick'
+    'click .git-ssh': ->
+      Cu.Helpers.showOrAddSSH @model.get('box')

   initialize: ->
     @model.on 'change', @render, this


options to set value, warn on not found

options to set value, warn on not found

--- client/lanes/views/Helpers.coffee
+++ client/lanes/views/Helpers.coffee
@@ -58,17 +58,17 @@

     text_field: (name,options={})->
         impl.field(name, options,
-            "<input type='#{options.type || 'text'}' name='#{name}'>"
+            "<input type='#{options.type || 'text'}' name='#{name}' value='#{options.value}'>"
             "

"
         )

     subview: (name,options={})->
-        definition = Lanes.Views.RenderContext.view().subviews[name]
-        selector = if definition.hook
-            "data-hook='#{definition.hook}'"
-        else
-            "class='#{definition.selector}'"
-        impl.field( name, options, "

" )
+        view = Lanes.Views.RenderContext.view()
+        definition = view.subviews[name]
+        if !definition
+            Lanes.warn("Subview #{name} not found on View #{Lanes.u.path(this.FILE)}")
+            return
+        impl.field( name, options, "

" )


Add a test on minimum Casper version

Add a test on minimum Casper version

--- client/tests/casper/common.coffee
+++ client/tests/casper/common.coffee
@@ -2,6 +2,9 @@
 utils = require "/usr/local/lib/node_modules/casperjs/modules/utils.js"

 dev = false
+
+if utils.cmpVersion("1.1", phantom.casperVersion) > 0
+    casper.die "You need at least CasperJS 1.1"

 exports.init = (casper) ->
     dev = casper.cli.options.dev?


More debugging

More debugging

--- client/user/userController.coffee
+++ client/user/userController.coffee
@@ -19,6 +19,8 @@
           logger.debug("Server was able to successfully get SoundCloud embeddables:", result)
           Session.set("soundCloudEmbeddables", result)
       )
+    else
+      logger.debug("Not loading media data from the server:", data, tabName)
   action: ->
     data = @data()
     # TODO: Consolidate with ProjectController


Increase timeout to 5 seconds

Increase timeout to 5 seconds

--- services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -5,7 +5,7 @@
 	"libs/bib-log-parser"
 	"services/log-hints-feedback"
 ], (App, Ace, HumanReadableLogs, BibLogParser) ->
-	AUTO_COMPILE_TIMEOUT = 3000
+	AUTO_COMPILE_TIMEOUT = 5000

 	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) ->
 		# enable per-user containers by default


remember window reference internally

remember window reference internally

--- coach/src/user/login-gateway.cjsx
+++ coach/src/user/login-gateway.cjsx
@@ -3,6 +3,8 @@
 User  = require './model'
 api   = require '../api'
 classnames = require 'classnames'
+
+CURRENT_WINDOW = undefined

 SECOND = 1000

@@ -41,11 +43,10 @@

       url = @urlForLogin()
       url += '?go=signup' if options.type is 'signup'
-      windowImpl.open(url, 'oxlogin', options)
+      CURRENT_WINDOW = windowImpl.open(url, 'oxlogin', options)

   propTypes:
-    loginWindow: WindowPropType.isRequired
     windowImpl: WindowPropType
     loginType: React.PropTypes.string
     onLogin:   React.PropTypes.func.isRequired
@@ -54,7 +55,7 @@
     windowImpl: window

   getInitialState: ->
-    loginWindow: @props.loginWindow
+    loginWindow: CURRENT_WINDOW

   openLogin: (ev) ->
     ev?.preventDefault()


tests: test `expectEventSpyBehavior` test util

--- coffee/test/util/test.spec.coffee
+++ coffee/test/util/test.spec.coffee
@@ -29,6 +29,7 @@
       expect(test.throwsExceptionWithString '42', fn).toBe false
       expect(test.throwsExceptionWithString '42', fn, []).toBe false
       expect(test.throwsExceptionWithString '42', fn, [ 'ey' ]).toBe false
+

   describe 'test.describeView', ->
     class View extends athena.lib.View
@@ -119,7 +120,6 @@
       checkDOM: (subEl, el) -> subEl.parentNode.parentNode is el

-
   describe 'EventSpy', ->
     it 'should be a function', ->
       expect(_.isFunction test.EventSpy).toBe true
@@ -223,3 +223,104 @@
       expect(s.arguments).toEqual []
       m.trigger 'b', {pedro: 'ah'}
       expect(s.arguments).toEqual [['b', {pedro: 'ah'}]]
+
+
+  describe 'expectEventSpyBehaviors', ->
+
+    it 'should be a function', ->
+      expect(typeof test.expectEventSpyBehaviors).toBe 'function'
+
+    it 'should test that events were triggered', ->
+      m = new Backbone.Model
+      spies =
+        seeSpy: new test.EventSpy m, 'see'
+        hearSpy: new test.EventSpy m, 'hear'
+        speakSpy: new test.EventSpy m, 'speak'
+        allSpy: new test.EventSpy m, 'all'
+
+      fns = [
+        ->
+          m.trigger 'see'
+          expectations =
+            seeSpy: undefined
+            allSpy: undefined
+        ->
+          m.trigger 'hear'
+          expectations =
+            hearSpy: undefined
+            allSpy: undefined
+        ->
+          m.trigger 'speak'
+          expectations =
+            speakSpy: undefined
+            allSpy: undefined
+        ->
+          m.trigger 'see'
+          m.trigger 'hear'
+          expectations =
+            seeSpy: undefined
+            hearSpy: undefined
+            allSpy: undefined
+        ->
+          m.trigger 'see'
+          m.trigger 'hear'
+          m.trigger 'speak'
+          expectations =
+            seeSpy: undefined
+            hearSpy: undefined
+            speakSpy: undefined
+            allSpy: undefined
+      ]
+
+      test.expectEventSpyBehaviors spies, fns
+
+    it 'should test that events were triggered with specific arguments', ->
+      m = new Backbone.Model
+      spies =
+        seeSpy: new test.EventSpy m, 'see'
+        hearSpy: new test.EventSpy m, 'hear'
+        speakSpy: new test.EventSpy m, 'speak'
+        allSpy: new test.EventSpy m, 'all'
+
+      fns = [
+        ->
+          evil = 'murdering children'
+          m.trigger 'see', evil
+          expectations =
+            seeSpy: evil
+            allSpy: ['see', evil]
+        ->
+          evil = 'tripping an old lady'
+          m.trigger 'hear', evil
+          expectations =
+            hearSpy: evil
+            allSpy: ['hear', evil]
+        ->
+          evil = 'stealing candy from children'
+          m.trigger 'speak', evil
+          expectations =
+            speakSpy: evil
+            allSpy: ['speak', evil]
+        ->
+          evil = 'slipping a laxative into the bake sale cookies'
+          m.trigger 'hear', evil
+          m.trigger 'speak', evil
+          expectations =
+            hearSpy: evil
+            speakSpy: evil
+            allSpy: ['speak', evil]
+        ->
+          evil = 'throwing a red ball in front of a blind man\'s guide dog, and
+              yelling fetch'
+          m.trigger 'speak', evil
+          m.trigger 'hear', evil
+          m.trigger 'see', evil
+          expectations =
+            seeSpy: evil
+            hearSpy: evil
+            speakSpy: evil
+            allSpy: ['see', evil]
+      ]
+
+      test.expectEventSpyBehaviors spies, fns
+


enable some passing object tests

enable some passing object tests

--- test/objects.coffee
+++ test/objects.coffee
@@ -77,31 +77,30 @@

   suite 'Implicit Objects', ->

-    #test 'implicit object literals', ->
-    #
-    #  obj =
-    #    a: 1,
-    #    b: 2,
-    #  ok obj.a is 1
-    #  ok obj.b is 2
-    #
-    #  config =
-    #    development:
-    #      server: 'localhost'
-    #      timeout: 10
-    #    production:
-    #      server: 'dreamboat'
-    #      timeout: 1000
-    #  ok config.development.server  is 'localhost'
-    #  ok config.production.server   is 'dreamboat'
-    #  ok config.development.timeout is 10
-    #  ok config.production.timeout  is 1000
-
-    #test 'implicit objects as part of chained calls', ->
-    #  pluck = (x) -> x.a
-    #  eq 100, pluck pluck pluck a: a: a: 100
-
-    #test 'explicit objects nested under implicit objects', ->
+    test 'implicit object literals', ->
+      obj =
+        a: 1
+        b: 2
+      ok obj.a is 1
+      ok obj.b is 2
+
+      config =
+        development:
+          server: 'localhost'
+          timeout: 10
+        production:
+          server: 'dreamboat'
+          timeout: 1000
+      eq config.development.server, 'localhost'
+      eq config.production.server, 'dreamboat'
+      eq config.development.timeout, 10
+      eq config.production.timeout, 1000
+
+    test 'implicit objects as part of chained calls', ->
+      pluck = (x) -> x.a
+      eq 100, pluck pluck pluck a: a: a: 100
+
+    test 'explicit objects nested under implicit objects', ->

     #test 'invoking functions with implicit object literals', ->
     #  generateGetter = (prop) -> (obj) -> obj[prop]
@@ -163,16 +162,16 @@
     #
     #  throws -> CoffeeScript.compile 'a = b:1, c'

-    #test 'multiple dedentations in implicit object literals', ->
-    #  nonce0 = {}
-    #  nonce1 = {}
-    #  obj =
-    #    a:
-    #      b: ->
-    #        c: nonce0
-    #    d: nonce1
-    #  eq nonce0, obj.a.b().c
-    #  eq nonce1, obj.d
+    test 'multiple dedentations in implicit object literals', ->
+      nonce0 = {}
+      nonce1 = {}
+      obj =
+        a:
+          b: ->
+            c: nonce0
+        d: nonce1
+      eq nonce0, obj.a.b().c
+      eq nonce1, obj.d

     #test 'jashkenas/coffee-script#1871: Special case for IMPLICIT_END in the middle of an implicit object', ->
     #  result = 'result'
@@ -206,11 +205,10 @@
     #
     #  eq obj.four, 4

-    #test 'jashkenas/coffee-script#2207: Immediate implicit closes don't close implicit objects', ->
-    #  func = ->
-    #    key: for i in [1, 2, 3] then i
-    #
-    #  eq func().key.join(' '), '1 2 3'
+    test 'jashkenas/coffee-script#2207: Immediate implicit closes don not close implicit objects', ->
+      func = ->
+        key: for i in [1, 2, 3] then i
+      eq func().key.join(' '), '1 2 3'

     test '#122 implicit object literal in conditional body', ->
       a = yes


some more tests

some more tests

--- test/xq-test.coffee
+++ test/xq-test.coffee
@@ -674,6 +674,15 @@
                 f.should.have.been.calledThrice
                 done()

+        it 'is ok with array of array', (done) ->
+
+            c = 1
+            X([[1,2],[3,4]]).each f = spy ([a,b]) ->
+                b.should.eql a + 1
+            .onEnd ->
+                f.callCount.should.eql 2
+                done()
+
         it 'is aliased to each', ->

             X::forEach.should.equal X::each
@@ -691,8 +700,27 @@
                 def.promise
             .then ->
                 c--
+            .onEnd ->
+                c.should.eql 0
+                done()
+            .done()
+
+        it 'works with events of events', (done) ->
+
+            c = 0
+            X([0,1,2,3,4,5]).singly (v) ->
+                c++
+                c.should.eql 1
+                X([v..(v+4)]).singly (v2) ->
+                    def = X.defer()
+                    later -> def.resolve v
+                    def.promise
+                .settle()
+            .then ->
+                c--
             .onEnd -> done()
             .done()
+

         it 'turns any incoming array which fails serially', (done) ->


Expand Player spec to test turning, and running (fully).

Includes refactoring testStartAnimation.

--- tests/player.coffee
+++ tests/player.coffee
@@ -14,7 +14,7 @@
       player = new Player()
       spyOn(player, method).and.callThrough() for method in [
         '_beginJump', '_buildJump', '_endJump'
-        '_beginRun'
+        '_beginRun', '_buildRun', '_endRun'
         '_beginTurn', '_endTurn'
         '_playAnimation'
       ]
@@ -49,7 +49,8 @@
       return if stopAt is 'landing'

     runRunUpdatesUntil = (stopAt, options) ->
-      if options?.backwards then player.cursors.left.isDown = yes
+      if options?.backwards and not (stopAt in ['turn', 'restart'])
+        player.cursors.left.isDown = yes
       else player.cursors.right.isDown = yes

       player.update()
@@ -59,11 +60,35 @@
       player.update()
       return if stopAt is 'running'

+      unless options?.backwards
+        player.velocity.x = 1
+        player.cursors.right.isDown = no
+        player.cursors.right.isUp = yes
+        player.update()
+        return if stopAt is 'stop'
+
+        player.velocity.x = 0
+        player.update()
+        return if stopAt is 'still'
+
+      player.cursors.right.isDown = no
+      player.cursors.left.isDown = yes
+      player.update()
+      return if stopAt is 'turn'
+
+      endAnimation()
+      player.update()
+      return if stopAt is 'restart'
+
     testStartAnimation = ->
-      it 'will play start animation', ->
-        expect(player.nextAction).toBe 'start'
-        expect(player._playAnimation).toHaveBeenCalledWith 'start', undefined
-        expect(player._isAnimationInterruptible()).toBe no
+      expect(player.nextAction).toBe 'start'
+      expect(player._playAnimation).toHaveBeenCalledWith 'start', undefined
+      expect(player._isAnimationInterruptible()).toBe no
+
+    testStopAnimation = ->
+      expect(player.nextAction).toBe 'stop'
+      expect(player._playAnimation).toHaveBeenCalledWith 'stop', yes
+      expect(player._isAnimationInterruptible()).toBe no

     describe 'when initialized', ->
       it 'is set to still and facing right', ->
@@ -91,19 +116,39 @@
           expect(player.state).toBe 'running'
           expect(player._beginRun).toHaveBeenCalled()

-        testStartAnimation()
+        it 'will play start animation', testStartAnimation

       describe 'when start animation finishes', ->
         beforeEach -> runRunUpdatesUntil 'running'

         it 'will be fully running', ->
           expect(player._isFullyRunning()).toBe yes
+          expect(player._buildRun).toHaveBeenCalled()

         it 'will play interruptible run animation in loop', ->
           expect(player._playAnimation).toHaveBeenCalledWith 'run', no
           expect(player.animation.loop).toBe on
           expect(player._isAnimationInterruptible()).toBe yes

+    describe 'when x cursor key was down but is up in same direction', ->
+
+      it 'will stop running and cancel turns', ->
+        runRunUpdatesUntil 'stop'
+
+        expect(player._endRun).toHaveBeenCalled()
+
+      it 'will play interrupting stop animation', ->
+        runRunUpdatesUntil 'stop'
+
+        testStopAnimation()
+
+      it 'will become still', ->
+        runRunUpdatesUntil 'still'
+
+        expect(player.state).toBe 'still'
+        expect(player._isTurning).toBe no
+        expect(player._endRun).toHaveBeenCalled()
+
     describe 'when x cursor key is down in opposite direction', ->
       it 'can get and set the right direction from #_xDirectionInput', ->
         runRunUpdatesUntil 'start', backwards: yes
@@ -119,9 +164,26 @@
           expect(player.direction).toBe Player.Direction.Left
           expect(player._beginRun).toHaveBeenCalled()

-        testStartAnimation()
-
-    # TODO: Test turning.
+        it 'will play start animation', testStartAnimation
+
+      describe 'when running in original direction', ->
+        beforeEach -> runRunUpdatesUntil 'turn', backwards: yes
+
+        it 'will begin turn', ->
+          expect(player._isTurning).toBe yes
+          expect(player.direction).toBe Player.Direction.Left
+          expect(player._beginTurn).toHaveBeenCalled()
+
+        it 'will play interrupting stop animation', testStopAnimation
+
+      describe 'when turning to opposite direction', ->
+        beforeEach -> runRunUpdatesUntil 'restart', backwards: yes
+
+        it 'will end turn', ->
+          expect(player._isTurning).toBe no
+          expect(player._endTurn).toHaveBeenCalled()
+
+        it 'will play start animation', testStartAnimation

     describe 'when up key is down', ->
       describe 'when still', ->


updated path

updated path

--- grunt/angular-templates.coffee
+++ grunt/angular-templates.coffee
@@ -10,7 +10,7 @@
           # To save ourselves duplicating code, we take the [`assets/js/partials.coffee`](partials.html) file,
           # compile it to JS and then inject the compiled templates in; this then forms our `$templateCache` bootstrap.
           src = grunt.config 'ngtemplates.prod.dest'
-          file = grunt.file.read src.replace '.js', '.coffee'
+          file = grunt.file.read src.replace /js/g, 'coffee'
           template = cs.compile file
           template.replace 'return \'#{script}\'', script
         htmlmin:


wip video control UI

wip video control UI

--- components/list/KDListView.coffee
+++ components/list/KDListView.coffee
@@ -52,10 +52,10 @@

   removeItem:(itemInstance, itemData, index)->

-    if index
+    if index?
       @emit 'ItemIsBeingDestroyed', { view : @items[index], index : index }
-      @items.splice index,1
-      item.destroy()
+      item = @items.splice index,1
+      item[0].destroy()
       return
     else
       for item,i in @items


Pull full trait data into the full character response

--- api/tools/gw2api.coffee
+++ api/tools/gw2api.coffee
@@ -217,7 +217,80 @@
     getCharacter: (name, cb) ->
         # Assuming the cache database isn't tampered with, these should always be available.
         name = encodeURIComponent name
-        requestWithCache "characters/#{name}", ['account', 'characters'], cb
+        requestWithCache "characters/#{name}", ['account', 'characters'], (err, char) ->
+            return (cb)(err, null) if err?
+
+            specializationIDs = []
+            traitIDs = []
+
+            _.each char.specializations, (mode) ->
+                _.each mode, (spec) ->
+                    if spec?
+                        specializationIDs.push spec.id
+                        traitIDs = traitIDs.concat spec.traits
+
+            if specializationIDs.length > 0
+                requestWithCache 'specializations?ids=' + _.compact(_.uniq(specializationIDs)).join(','), [], (err, specs) ->
+                    return (cb)(err, null) if err?
+
+                    _.each specs.data, (spec) ->
+                        traitIDs = traitIDs.concat spec.minor_traits
+
+                    specs = _.indexBy specs.data, 'id'
+                    _.each char.specializations, (mode) ->
+                        _.each mode, (spec) ->
+                            if spec?
+                                spec.name = specs[spec.id].name
+                                spec.icon = specs[spec.id].icon
+                                spec.background = specs[spec.id].background
+                                spec.traits = spec.traits.concat specs[spec.id].minor_traits
+
+                    requestWithCache 'traits?ids=' + _.compact(_.uniq(traitIDs)).join(','), [], (err, traits) ->
+                        return (cb)(err, null) if err?
+
+                        traits = _.indexBy traits.data, 'id'
+
+                        _.each char.specializations, (mode) ->
+                            _.each mode, (spec) ->
+                                if spec?
+                                    traitData = []
+                                    _.each spec.traits, (trait) ->
+                                        traitData.push _.cloneDeep traits[trait] if trait?
+
+                                    spec.traits = _.sortBy traitData, (trait) ->
+                                        trait.tier * 2 + (if trait.slot == 'Minor' then 0 else 1)
+
+                            # Handle trait synergy based on the other traits chosen in that game mode.
+                            modeTraits = []
+                            _.each mode, (spec) ->
+                                if spec?
+                                    modeTraits = modeTraits.concat _.pluck spec.traits, 'id'
+
+                            _.each mode, (spec) ->
+                                if spec?
+                                    _.each spec.traits, (trait) ->
+                                        _.each trait.facts, (fact) ->
+                                            fact.traited = false
+
+                                        if trait.traited_facts?
+                                            _.each trait.traited_facts, (tfact) ->
+                                                if tfact.requires_trait in modeTraits
+                                                    tfact.traited = true
+
+                                                    if tfact.overrides?
+                                                        trait.facts[tfact.overrides] = tfact
+                                                    else
+                                                        if trait.facts?
+                                                            trait.facts.push tfact
+                                                        else
+                                                            trait.facts = [tfact]
+
+                        (cb)(err, char)
+
+            else
+                (cb)(err, char)
+
+
     getBank: (cb) ->
         async.parallel
             bank: (asyncCb) -> requestWithCache 'account/bank', ['account', 'inventories'], asyncCb


fix typos

fix typos

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -15,7 +15,7 @@
         # defer if binder calls handler in sync before returning unbinder
         if unbinder? then unbinder() else setTimeout (-> unbinder()), 0

-# eventTransformer - defaults to returning the first agrument to handler
+# eventTransformer - defaults to returning the first argument to handler
 Bacon.$ = asEventStream: (eventName, selector, eventTransformer) ->
   [eventTransformer, selector] = [selector, null] if isFunction(selector)
   Bacon.fromBinder (handler) =>
@@ -31,7 +31,7 @@
 #
 # target - EventTarget or EventEmitter, source of events
 # eventName - event name to bind
-# eventTransformer - defaults to returning the first agrument to handler
+# eventTransformer - defaults to returning the first argument to handler
 #
 # Examples
 #


re-implement  to correctly support simultaneous values

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -506,7 +506,9 @@

   awaiting: (other) ->
     withDescription(this, "awaiting", other,
-    this.toEventStream().map(true).merge(other.toEventStream().map(false)).toProperty(false))
+      Bacon.groupSimultaneous(this, other)
+        .map(([myValues, otherValues]) -> otherValues.length == 0)
+        .toProperty(false))

 Observable :: reduce = Observable :: fold