Bug 839862 - The developer toolbar / GCLI should be able to execute commands remotely; r=dcamp
☠☠ backed out by dd5e3232a63c ☠ ☠
authorJoe Walker <jwalker@mozilla.com>
Thu, 09 May 2013 15:15:22 +0100
changeset 142495 d6249744132e202e60b8dd3d756518beef692a9e
parent 142494 0913fd986ec1bc8a22997393c589b320636b67ca
child 142496 b05c947fa8aecca548ed88bdee63b4c8665425e3
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs839862
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 839862 - The developer toolbar / GCLI should be able to execute commands remotely; r=dcamp
browser/devtools/commandline/BuiltinCommands.jsm
browser/devtools/commandline/gcli.jsm
browser/devtools/commandline/test/Makefile.in
browser/devtools/commandline/test/browser_cmd_appcache_valid.js
browser/devtools/commandline/test/browser_cmd_commands.js
browser/devtools/commandline/test/browser_cmd_cookie.html
browser/devtools/commandline/test/browser_cmd_cookie.js
browser/devtools/commandline/test/browser_gcli_canon.js
browser/devtools/commandline/test/browser_gcli_cli.js
browser/devtools/commandline/test/browser_gcli_completion.js
browser/devtools/commandline/test/browser_gcli_context.js
browser/devtools/commandline/test/browser_gcli_fail.js
browser/devtools/commandline/test/browser_gcli_inputter.js
browser/devtools/commandline/test/browser_gcli_resource.js
browser/devtools/commandline/test/browser_gcli_split.js
browser/devtools/commandline/test/browser_gcli_tokenize.js
browser/devtools/commandline/test/browser_gcli_types.js
browser/devtools/commandline/test/helpers.js
browser/devtools/commandline/test/mockCommands.js
browser/devtools/debugger/CmdDebugger.jsm
browser/devtools/debugger/test/helpers.js
browser/devtools/inspector/test/helpers.js
browser/devtools/responsivedesign/test/helpers.js
browser/devtools/shared/DeveloperToolbar.jsm
browser/devtools/styleeditor/test/helpers.js
browser/locales/en-US/chrome/browser/devtools/gcli.properties
browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
toolkit/devtools/debugger/server/dbg-server.js
toolkit/devtools/gcli/Makefile.in
toolkit/devtools/gcli/dbg-gcli-actors.js
toolkit/devtools/jar.mn
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -4,22 +4,24 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
                            .getService(Ci.nsIStringBundleService)
                            .createBundle("chrome://branding/locale/brand.properties")
                            .GetStringFromName("brandShortName");
 
-this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands" ];
+this.EXPORTED_SYMBOLS = [ "CmdAddonFlags", "CmdCommands", "DEFAULT_DEBUG_PORT", "connect" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource://gre/modules/osfile.jsm")
 
 Cu.import("resource:///modules/devtools/gcli.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource:///modules/devtools/gDevTools.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppCacheUtils",
                                   "resource:///modules/devtools/AppCacheUtils.jsm");
@@ -32,26 +34,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
   // We need to use an object in which to store any flags because a primitive
   // would remain undefined.
   module.CmdAddonFlags = {
     addonsLoaded: false
   };
 
   /**
-  * 'addon' command.
-  */
+   * 'addon' command.
+   */
   gcli.addCommand({
     name: "addon",
     description: gcli.lookup("addonDesc")
   });
 
   /**
-  * 'addon list' command.
-  */
+   * 'addon list' command.
+   */
   gcli.addCommand({
     name: "addon list",
     description: gcli.lookup("addonListDesc"),
     returnType: "addonsInfo",
     params: [{
       name: 'type',
       type: {
         name: 'selection',
@@ -256,21 +258,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     * 'addon enable' command.
     */
     gcli.addCommand({
       name: "addon enable",
       description: gcli.lookup("addonEnableDesc"),
       params: [nameParameter],
       exec: function(aArgs, context) {
         /**
-        * Enables the addon in the passed list which has a name that matches
-        * according to the passed name comparer, and resolves the promise which
-        * is the scope (this) of this function to display the result of this
-        * enable attempt.
-        */
+         * Enables the addon in the passed list which has a name that matches
+         * according to the passed name comparer, and resolves the promise which
+         * is the scope (this) of this function to display the result of this
+         * enable attempt.
+         */
         function enable(aName, addons) {
           // Find the add-on.
           let addon = null;
           addons.some(function(candidate) {
             if (candidate.name == aName) {
               addon = candidate;
               return true;
             } else {
@@ -293,18 +295,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
         let deferred = context.defer();
         // List the installed add-ons, enable one when done listing.
         AddonManager.getAllAddons(enable.bind(deferred, aArgs.name));
         return deferred.promise;
       }
     });
 
     /**
-    * 'addon disable' command.
-    */
+     * 'addon disable' command.
+     */
     gcli.addCommand({
       name: "addon disable",
       description: gcli.lookup("addonDisableDesc"),
       params: [nameParameter],
       exec: function(aArgs, context) {
         /**
         * Like enable, but ... you know ... the exact opposite.
         */
@@ -405,32 +407,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     JsDebugger.addDebuggerToGlobal(global);
 
     return global.Debugger;
   });
 
   let debuggers = [];
 
   /**
-  * 'calllog' command
-  */
+   * 'calllog' command
+   */
   gcli.addCommand({
     name: "calllog",
     description: gcli.lookup("calllogDesc")
   })
 
   /**
-  * 'calllog start' command
-  */
+   * 'calllog start' command
+   */
   gcli.addCommand({
     name: "calllog start",
     description: gcli.lookup("calllogStartDesc"),
 
     exec: function(args, context) {
-      let contentWindow = context.environment.contentDocument.defaultView;
+      let contentWindow = context.environment.window;
 
       let dbg = new Debugger(contentWindow);
       dbg.onEnterFrame = function(frame) {
         // BUG 773652 -  Make the output from the GCLI calllog command nicer
         contentWindow.console.log("Method call: " + this.callDescription(frame));
       }.bind(this);
 
       debuggers.push(dbg);
@@ -462,18 +464,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       if (typeof value !== "object" || value === null) {
         return uneval(value);
       }
       return "[object " + value.class + "]";
     }
   });
 
   /**
-  * 'calllog stop' command
-  */
+   * 'calllog stop' command
+   */
   gcli.addCommand({
     name: "calllog stop",
     description: gcli.lookup("calllogStopDesc"),
 
     exec: function(args, context) {
       let numDebuggers = debuggers.length;
       if (numDebuggers == 0) {
         return gcli.lookup("calllogStopNoLogging");
@@ -501,18 +503,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
     return global.Debugger;
   });
 
   let debuggers = [];
   let sandboxes = [];
 
   /**
-  * 'calllog chromestart' command
-  */
+   * 'calllog chromestart' command
+   */
   gcli.addCommand({
     name: "calllog chromestart",
     description: gcli.lookup("calllogChromeStartDesc"),
     get hidden() gcli.hiddenByChromePref(),
     params: [
       {
         name: "sourceType",
         type: {
@@ -524,17 +526,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
         name: "source",
         type: "string",
         description: gcli.lookup("calllogChromeSourceTypeDesc"),
         manual: gcli.lookup("calllogChromeSourceTypeManual"),
       }
     ],
     exec: function(args, context) {
       let globalObj;
-      let contentWindow = context.environment.contentDocument.defaultView;
+      let contentWindow = context.environment.window;
 
       if (args.sourceType == "jsm") {
         try {
           globalObj = Cu.import(args.source);
         }
         catch (e) {
           return gcli.lookup("callLogChromeInvalidJSM");
         }
@@ -603,18 +605,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     callDescription: function(frame) {
       let name = frame.callee.name || gcli.lookup("callLogChromeAnonFunction");
       let args = frame.arguments.map(this.valueToString).join(", ");
       return name + "(" + args + ")";
     }
   });
 
   /**
-  * 'calllog chromestop' command
-  */
+   * 'calllog chromestop' command
+   */
   gcli.addCommand({
     name: "calllog chromestop",
     description: gcli.lookup("calllogChromeStopDesc"),
     get hidden() gcli.hiddenByChromePref(),
     exec: function(args, context) {
       let numDebuggers = debuggers.length;
       if (numDebuggers == 0) {
         return gcli.lookup("calllogChromeStopNoLogging");
@@ -647,32 +649,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                     "resource://gre/modules/NetUtil.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "console",
                                     "resource://gre/modules/devtools/Console.jsm");
 
   const PREF_DIR = "devtools.commands.dir";
 
   /**
-  * A place to store the names of the commands that we have added as a result of
-  * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
-  * added commands.
-  */
+   * A place to store the names of the commands that we have added as a result of
+   * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
+   * added commands.
+   */
   let commands = [];
 
   /**
-  * Exported API
-  */
+   * Exported API
+   */
   this.CmdCommands = {
     /**
-    * Called to look in a directory pointed at by the devtools.commands.dir pref
-    * for *.mozcmd files which are then loaded.
-    * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
-    * we eval the script from the .mozcmd file. This should be a chrome window.
-    */
+     * Called to look in a directory pointed at by the devtools.commands.dir pref
+     * for *.mozcmd files which are then loaded.
+     * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+     * we eval the script from the .mozcmd file. This should be a chrome window.
+     */
     refreshAutoCommands: function GC_refreshAutoCommands(aSandboxPrincipal) {
       // First get rid of the last set of commands
       commands.forEach(function(name) {
         gcli.removeCommand(name);
       });
 
       let dirName = prefBranch.getComplexValue(PREF_DIR,
                                               Ci.nsISupportsString).data.trim();
@@ -729,22 +731,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
             }
           );
         }
       );
     }
   };
 
   /**
-  * Load the commands from a single file
-  * @param OS.File.DirectoryIterator.Entry aFileEntry The DirectoryIterator
-  * Entry of the file containing the commands that we should read
-  * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
-  * we eval the script from the .mozcmd file. This should be a chrome window.
-  */
+   * Load the commands from a single file
+   * @param OS.File.DirectoryIterator.Entry aFileEntry The DirectoryIterator
+   * Entry of the file containing the commands that we should read
+   * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+   * we eval the script from the .mozcmd file. This should be a chrome window.
+   */
   function loadCommandFile(aFileEntry, aSandboxPrincipal) {
     let promise = OS.File.read(aFileEntry.path);
     promise = promise.then(
       function onSuccess(array) {
         let decoder = new TextDecoder();
         let source = decoder.decode(array);
 
         let sandbox = new Cu.Sandbox(aSandboxPrincipal, {
@@ -768,27 +770,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       function onError(reason) {
         console.error("OS.File.read(" + aFileEntry.path + ") failed.");
         throw reason;
       }
     );
   }
 
   /**
-  * 'cmd' command
-  */
+   * 'cmd' command
+   */
   gcli.addCommand({
     name: "cmd",
     get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
     description: gcli.lookup("cmdDesc")
   });
 
   /**
-  * 'cmd refresh' command
-  */
+   * 'cmd refresh' command
+   */
   gcli.addCommand({
     name: "cmd refresh",
     description: gcli.lookup("cmdRefreshDesc"),
     get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
     exec: function Command_cmdRefresh(args, context) {
       let chromeWindow = context.environment.chromeDocument.defaultView;
       CmdCommands.refreshAutoCommands(chromeWindow);
     }
@@ -797,56 +799,55 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 /* CmdConsole -------------------------------------------------------------- */
 
 (function(module) {
   XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
                                     "resource:///modules/HUDService.jsm");
 
   /**
-  * 'console' command
-  */
+   * 'console' command
+   */
   gcli.addCommand({
     name: "console",
     description: gcli.lookup("consoleDesc"),
     manual: gcli.lookup("consoleManual")
   });
 
   /**
-  * 'console clear' command
-  */
+   * 'console clear' command
+   */
   gcli.addCommand({
     name: "console clear",
     description: gcli.lookup("consoleclearDesc"),
     exec: function Command_consoleClear(args, context) {
-      let window = context.environment.contentDocument.defaultView;
-      let hud = HUDService.getHudByWindow(window);
+      let hud = HUDService.getHudByWindow(context.environment.window);
       // hud will be null if the web console has not been opened for this window
       if (hud) {
         hud.jsterm.clearOutput();
       }
     }
   });
 
   /**
-  * 'console close' command
-  */
+   * 'console close' command
+   */
   gcli.addCommand({
     name: "console close",
     description: gcli.lookup("consolecloseDesc"),
     exec: function Command_consoleClose(args, context) {
       let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
       let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
       return gDevTools.closeToolbox(target);
     }
   });
 
   /**
-  * 'console open' command
-  */
+   * 'console open' command
+   */
   gcli.addCommand({
     name: "console open",
     description: gcli.lookup("consoleopenDesc"),
     exec: function Command_consoleOpen(args, context) {
       let gBrowser = context.environment.chromeDocument.defaultView.gBrowser;
       let target = devtools.TargetFactory.forTab(gBrowser.selectedTab);
       return gDevTools.showToolbox(target, "webconsole");
     }
@@ -1018,17 +1019,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
     ],
     exec: function Command_cookieRemove(args, context) {
       let host = context.environment.document.location.host;
       let enm = cookieMgr.getCookiesFromHost(host);
 
       let cookies = [];
       while (enm.hasMoreElements()) {
-        let cookie = enm.getNext().QueryInterface(Components.interfaces.nsICookie);
+        let cookie = enm.getNext().QueryInterface(Ci.nsICookie);
         if (isCookieAtHost(cookie, host)) {
           if (cookie.name == args.name) {
             cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
           }
         }
       }
     }
   });
@@ -1151,55 +1152,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           visible: true,
           typed: command
         });
       });
     }
   }
 }(this));
 
-/* CmdEcho ----------------------------------------------------------------- */
-
-(function(module) {
-  /**
-  * 'echo' command
-  */
-  gcli.addCommand({
-    name: "echo",
-    description: gcli.lookup("echoDesc"),
-    params: [
-      {
-        name: "message",
-        type: "string",
-        description: gcli.lookup("echoMessageDesc")
-      }
-    ],
-    returnType: "string",
-    hidden: true,
-    exec: function Command_echo(args, context) {
-      return args.message;
-    }
-  });
-}(this));
-
 /* CmdExport --------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'export' command
-  */
+   * 'export' command
+   */
   gcli.addCommand({
     name: "export",
     description: gcli.lookup("exportDesc"),
   });
 
   /**
-  * The 'export html' command. This command allows the user to export the page to
-  * HTML after they do DOM changes.
-  */
+   * The 'export html' command. This command allows the user to export the page to
+   * HTML after they do DOM changes.
+   */
   gcli.addCommand({
     name: "export html",
     description: gcli.lookup("exportHtmlDesc"),
     exec: function(args, context) {
       let html = context.environment.document.documentElement.outerHTML;
       let url = 'data:text/plain;charset=utf8,' + encodeURIComponent(html);
       context.environment.window.open(url);
     }
@@ -1211,18 +1188,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 (function(module) {
   const XMLHttpRequest =
     Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
 
   XPCOMUtils.defineLazyModuleGetter(this, "js_beautify",
                                     "resource:///modules/devtools/Jsbeautify.jsm");
 
   /**
-  * jsb command.
-  */
+   * jsb command.
+   */
   gcli.addCommand({
     name: 'jsb',
     description: gcli.lookup('jsbDesc'),
     returnValue:'string',
     params: [
       {
         name: 'url',
         type: 'string',
@@ -1338,27 +1315,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     }
   });
 }(this));
 
 /* CmdPagemod -------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'pagemod' command
-  */
+   * 'pagemod' command
+   */
   gcli.addCommand({
     name: "pagemod",
     description: gcli.lookup("pagemodDesc"),
   });
 
   /**
-  * The 'pagemod replace' command. This command allows the user to search and
-  * replace within text nodes and attributes.
-  */
+   * The 'pagemod replace' command. This command allows the user to search and
+   * replace within text nodes and attributes.
+   */
   gcli.addCommand({
     name: "pagemod replace",
     description: gcli.lookup("pagemodReplaceDesc"),
     params: [
       {
         name: "search",
         type: "string",
         description: gcli.lookup("pagemodReplaceSearchDesc"),
@@ -1398,27 +1375,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       {
         name: "attributes",
         type: "string",
         description: gcli.lookup("pagemodReplaceAttributesDesc"),
         defaultValue: null,
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
       let searchTextNodes = !args.attrOnly;
       let searchAttributes = !args.contentOnly;
       let regexOptions = args.ignoreCase ? 'ig' : 'g';
       let search = new RegExp(escapeRegex(args.search), regexOptions);
       let attributeRegex = null;
       if (args.attributes) {
         attributeRegex = new RegExp(args.attributes, regexOptions);
       }
 
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let elements = root.querySelectorAll(args.selector);
       elements = Array.prototype.slice.call(elements);
 
       let replacedTextNodes = 0;
       let replacedAttributes = 0;
 
       function replaceAttribute() {
         replacedAttributes++;
@@ -1455,27 +1431,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
       return gcli.lookupFormat("pagemodReplaceResult",
                               [elements.length, replacedTextNodes,
                                 replacedAttributes]);
     }
   });
 
   /**
-  * 'pagemod remove' command
-  */
+   * 'pagemod remove' command
+   */
   gcli.addCommand({
     name: "pagemod remove",
     description: gcli.lookup("pagemodRemoveDesc"),
   });
 
 
   /**
-  * The 'pagemod remove element' command.
-  */
+   * The 'pagemod remove element' command.
+   */
   gcli.addCommand({
     name: "pagemod remove element",
     description: gcli.lookup("pagemodRemoveElementDesc"),
     params: [
       {
         name: "search",
         type: "string",
         description: gcli.lookup("pagemodRemoveElementSearchDesc"),
@@ -1493,18 +1469,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       },
       {
         name: 'ifEmptyOnly',
         type: 'boolean',
         description: gcli.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
 
       let removed = 0;
       for (let i = 0; i < elements.length; i++) {
         let element = elements[i];
         let parentNode = element.parentNode;
         if (!parentNode || !element.removeChild) {
           continue;
@@ -1521,18 +1496,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
 
       return gcli.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
                               [elements.length, removed]);
     }
   });
 
   /**
-  * The 'pagemod remove attribute' command.
-  */
+   * The 'pagemod remove attribute' command.
+   */
   gcli.addCommand({
     name: "pagemod remove attribute",
     description: gcli.lookup("pagemodRemoveAttributeDesc"),
     params: [
       {
         name: "searchAttributes",
         type: "string",
         description: gcli.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
@@ -1550,19 +1525,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       },
       {
         name: "ignoreCase",
         type: "boolean",
         description: gcli.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
       },
     ],
     exec: function(args, context) {
-      let document = context.environment.contentDocument;
-
-      let root = args.root || document;
+      let root = args.root || context.environment.document;
       let regexOptions = args.ignoreCase ? 'ig' : 'g';
       let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
       let elements = root.querySelectorAll(args.searchElements);
       elements = Array.prototype.slice.call(elements);
 
       let removed = 0;
       for (let i = 0; i < elements.length; i++) {
         let element = elements[i];
@@ -1581,23 +1554,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
 
       return gcli.lookupFormat("pagemodRemoveAttributeResult",
                               [elements.length, removed]);
     }
   });
 
   /**
-  * Make a given string safe to use  in a regular expression.
-  *
-  * @param string aString
-  *        The string you want to use in a regex.
-  * @return string
-  *         The equivalent of |aString| but safe to use in a regex.
-  */
+   * Make a given string safe to use  in a regular expression.
+   *
+   * @param string aString
+   *        The string you want to use in a regex.
+   * @return string
+   *         The equivalent of |aString| but safe to use in a regex.
+   */
   function escapeRegex(aString) {
     return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
   }
 }(this));
 
 /* CmdTools -------------------------------------------------------------- */
 
 (function(module) {
@@ -1617,32 +1590,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       {
         name: "srcdir",
         type: "string",
         description: gcli.lookup("toolsSrcdirDir")
       }
     ],
     returnType: "string",
     exec: function(args, context) {
-      let promise = context.createPromise();
-      let existsPromise = OS.File.exists(args.srcdir + "/CLOBBER");
-      existsPromise.then(function(exists) {
+      return OS.File.exists(args.srcdir + "/CLOBBER").then(function(exists) {
         if (exists) {
-          var str = Cc["@mozilla.org/supports-string;1"]
-            .createInstance(Ci.nsISupportsString);
+          let str = Cc["@mozilla.org/supports-string;1"]
+                    .createInstance(Ci.nsISupportsString);
           str.data = args.srcdir;
           Services.prefs.setComplexValue("devtools.loader.srcdir",
-              Components.interfaces.nsISupportsString, str);
+                                         Ci.nsISupportsString, str);
           devtools.reload();
-          promise.resolve(gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir]));
-          return;
+
+          let msg = gcli.lookupFormat("toolsSrcdirReloaded", [args.srcdir]);
+          throw new Error(msg);
         }
-        promise.reject(gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir]));
+
+        return gcli.lookupFormat("toolsSrcdirNotFound", [args.srcdir]);
       });
-      return promise;
     }
   });
 
   gcli.addCommand({
     name: "tools builtin",
     description: gcli.lookup("toolsBuiltinDesc"),
     manual: gcli.lookup("toolsBuiltinManual"),
     get hidden() gcli.hiddenByChromePref(),
@@ -1666,28 +1638,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     }
   });
 }(this));
 
 /* CmdRestart -------------------------------------------------------------- */
 
 (function(module) {
   /**
-  * Restart command
-  *
-  * @param boolean nocache
-  *        Disables loading content from cache upon restart.
-  *
-  * Examples :
-  * >> restart
-  * - restarts browser immediately
-  * >> restart --nocache
-  * - restarts immediately and starts Firefox without using cache
-  */
-
+   * Restart command
+   *
+   * @param boolean nocache
+   *        Disables loading content from cache upon restart.
+   *
+   * Examples :
+   * >> restart
+   * - restarts browser immediately
+   * >> restart --nocache
+   * - restarts immediately and starts Firefox without using cache
+   */
   gcli.addCommand({
     name: "restart",
     description: gcli.lookupFormat("restartBrowserDesc", [BRAND_SHORT_NAME]),
     params: [
       {
         name: "nocache",
         type: "boolean",
         description: gcli.lookup("restartBrowserNocacheDesc")
@@ -1722,23 +1693,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
                                     "resource:///modules/devtools/LayoutHelpers.jsm");
 
   // String used as an indication to generate default file name in the following
   // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
   const FILENAME_DEFAULT_VALUE = " ";
 
   /**
-  * 'screenshot' command
-  */
+   * 'screenshot' command
+   */
   gcli.addCommand({
     name: "screenshot",
     description: gcli.lookup("screenshotDesc"),
     manual: gcli.lookup("screenshotManual"),
-    returnType: "html",
+    returnType: "dom",
     params: [
       {
         name: "filename",
         type: "string",
         defaultValue: FILENAME_DEFAULT_VALUE,
         description: gcli.lookup("screenshotFilenameDesc"),
         manual: gcli.lookup("screenshotFilenameManual")
       },
@@ -1783,34 +1754,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     exec: function Command_screenshot(args, context) {
       if (args.chrome && args.selector) {
         // Node screenshot with chrome option does not work as inteded
         // Refer https://bugzilla.mozilla.org/show_bug.cgi?id=659268#c7
         // throwing for now.
         throw new Error(gcli.lookup("screenshotSelectorChromeConflict"));
       }
       var document = args.chrome? context.environment.chromeDocument
-                                : context.environment.contentDocument;
+                                : context.environment.document;
       if (args.delay > 0) {
         var deferred = context.defer();
         document.defaultView.setTimeout(function Command_screenshotDelay() {
           let reply = this.grabScreen(document, args.filename, args.clipboard,
                                       args.fullpage);
           deferred.resolve(reply);
         }.bind(this), args.delay * 1000);
         return deferred.promise;
       }
       else {
         return this.grabScreen(document, args.filename, args.clipboard,
                               args.fullpage, args.selector);
       }
     },
-    grabScreen:
-    function Command_screenshotGrabScreen(document, filename, clipboard,
-                                          fullpage, node) {
+    grabScreen: function(document, filename, clipboard, fullpage, node) {
       let window = document.defaultView;
       let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
       let left = 0;
       let top = 0;
       let width;
       let height;
       let div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
 
@@ -1943,23 +1912,215 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                         "background-size: 256px " + previewHeight + "px;" +
                         "margin: 4px; display: block");
       div.appendChild(image);
       return div;
     }
   });
 }(this));
 
+
+/* Remoting ----------------------------------------------------------- */
+
+const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
+
+/**
+ * 'listen' command
+ */
+gcli.addCommand({
+  name: "listen",
+  description: gcli.lookup("listenDesc"),
+  manual: gcli.lookup("listenManual"),
+  params: [
+    {
+      name: "port",
+      type: "number",
+      get defaultValue() {
+        return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+      },
+      description: gcli.lookup("listenPortDesc"),
+    }
+  ],
+  exec: function Command_screenshot(args, context) {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init();
+      DebuggerServer.addBrowserActors();
+    }
+    var reply = DebuggerServer.openListener(args.port);
+    if (!reply) {
+      throw new Error(gcli.lookup("listenDisabledOutput"));
+    }
+
+    if (DebuggerServer.initialized) {
+      return gcli.lookupFormat("listenInitOutput", [ '' + args.port ]);
+    }
+
+    return gcli.lookup("listenNoInitOutput");
+  },
+});
+
+const {
+  debuggerSocketConnect, DebuggerClient
+} = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {});
+
+/**
+ * Create a Connection object and initiate a connection.
+ */
+function connect(prefix, host, port) {
+  let connection = new Connection(prefix, host, port);
+  return connection.connect().then(function() {
+    return connection;
+  });
+}
+
+/**
+ * Manage a named connection to an HTTP server over web-sockets using socket.io
+ */
+function Connection(prefix, host, port) {
+  this.prefix = prefix;
+  this.host = host;
+  this.port = port;
+
+  // Properties setup by connect()
+  this.actor = undefined;
+  this.transport = undefined;
+  this.client = undefined;
+
+  this.requests = {};
+  this.nextRequestId = 0;
+}
+
+/**
+ * Setup socket.io, retrieve the list of remote commands and register them with
+ * the local canon.
+ * @return a promise which resolves (to undefined) when the connection is made
+ * or is rejected (with an error message) if the connection fails
+ */
+Connection.prototype.connect = function() {
+  let deferred = Promise.defer();
+
+  this.transport = debuggerSocketConnect(this.host, this.port);
+  this.client = new DebuggerClient(this.transport);
+
+  this.client.connect(() => {
+    this.client.listTabs(response => {
+      this.actor = response.gcliActor;
+      deferred.resolve();
+    });
+  });
+
+  return deferred.promise;
+};
+
+/**
+ * Retrieve the list of remote commands.
+ * @return a promise of an array of commandSpecs
+ */
+Connection.prototype.getCommandSpecs = function() {
+  let deferred = Promise.defer();
+
+  let request = { to: this.actor, type: 'getCommandSpecs' };
+
+  this.client.request(request, (response) => {
+    deferred.resolve(response.commandSpecs);
+  });
+
+  return deferred.promise;
+};
+
+/**
+ * Send an execute request. Replies are handled by the setup in connect()
+ */
+Connection.prototype.execute = function(typed, cmdArgs) {
+  let deferred = Promise.defer();
+
+  let request = {
+    to: this.actor,
+    type: 'execute',
+    typed: typed,
+    args: cmdArgs
+  };
+
+  this.client.request(request, (response) => {
+    deferred.resolve(response.reply);
+  });
+
+  return deferred.promise;
+};
+
+/**
+ * Send an execute request.
+ */
+Connection.prototype.execute = function(typed, cmdArgs) {
+  var request = new Request(this.actor, typed, cmdArgs);
+  this.requests[request.json.id] = request;
+
+  this.client.request(request.json, (response) => {
+    let request = this.requests[response.id];
+    delete this.requests[response.id];
+
+    request.complete(response.error, response.type, response.data);
+  });
+
+  return request.promise;
+};
+
+/**
+ * Kill this connection
+ */
+Connection.prototype.disconnect = function() {
+  let deferred = Promise.defer();
+
+  this.client.close(() => {
+    deferred.resolve();
+  });
+
+  return request.promise;
+};
+
+/**
+ * A Request is a command typed at the client which lives until the command
+ * has finished executing on the server
+ */
+function Request(actor, typed, args) {
+  this.json = {
+    to: actor,
+    type: 'execute',
+    typed: typed,
+    args: args,
+    id: Request._nextRequestId++,
+  };
+
+  this._deferred = Promise.defer();
+  this.promise = this._deferred.promise;
+}
+
+Request._nextRequestId = 0;
+
+/**
+ * Called by the connection when a remote command has finished executing
+ * @param error boolean indicating output state
+ * @param type the type of the returned data
+ * @param data the data itself
+ */
+Request.prototype.complete = function(error, type, data) {
+  this._deferred.resolve({
+    error: error,
+    type: type,
+    data: data
+  });
+};
+
+
 /* CmdPaintFlashing ------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'paintflashing' command
-  */
-
+   * 'paintflashing' command
+   */
   gcli.addCommand({
     name: 'paintflashing',
     description: gcli.lookup('paintflashingDesc')
   });
 
   gcli.addCommand({
     name: 'paintflashing on',
     description: gcli.lookup('paintflashingOnDesc'),
@@ -1971,25 +2132,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "boolean",
           name: "chrome",
           get hidden() gcli.hiddenByChromePref(),
           description: gcli.lookup("paintflashingChromeDesc"),
         }
       ]
     }],
     exec: function(args, context) {
-      var window;
-      if (args.chrome) {
-        window = context.environment.chromeDocument.defaultView;
-      } else {
-        window = context.environment.contentDocument.defaultView;
-      }
-      window.QueryInterface(Ci.nsIInterfaceRequestor).
-             getInterface(Ci.nsIDOMWindowUtils).
-             paintFlashing = true;
+      var window = args.chrome ?
+                  context.environment.chromeWindow :
+                  context.environment.window;
+
+      window.QueryInterface(Ci.nsIInterfaceRequestor)
+            .getInterface(Ci.nsIDOMWindowUtils)
+            .paintFlashing = true;
       onPaintFlashingChanged(context);
     }
   });
 
   gcli.addCommand({
     name: 'paintflashing off',
     description: gcli.lookup('paintflashingOffDesc'),
     manual: gcli.lookup('paintflashingManual'),
@@ -2000,24 +2159,23 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "boolean",
           name: "chrome",
           get hidden() gcli.hiddenByChromePref(),
           description: gcli.lookup("paintflashingChromeDesc"),
         }
       ]
     }],
     exec: function(args, context) {
-      if (args.chrome) {
-        var window = context.environment.chromeDocument.defaultView;
-      } else {
-        var window = context.environment.contentDocument.defaultView;
-      }
-      window.QueryInterface(Ci.nsIInterfaceRequestor).
-             getInterface(Ci.nsIDOMWindowUtils).
-             paintFlashing = false;
+      var window = args.chrome ?
+                  context.environment.chromeWindow :
+                  context.environment.window;
+
+      window.QueryInterface(Ci.nsIInterfaceRequestor)
+            .getInterface(Ci.nsIDOMWindowUtils)
+            .paintFlashing = false;
       onPaintFlashingChanged(context);
     }
   });
 
   gcli.addCommand({
     name: 'paintflashing toggle',
     hidden: true,
     buttonId: "command-button-paintflashing",
@@ -2066,18 +2224,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     target.once("navigate", fireChange);
   }
 }(this));
 
 /* CmdAppCache ------------------------------------------------------- */
 
 (function(module) {
   /**
-  * 'appcache' command
-  */
+   * 'appcache' command
+   */
 
   gcli.addCommand({
     name: 'appcache',
     description: gcli.lookup('appCacheDesc')
   });
 
   gcli.addConverter({
     from: "appcacheerrors",
@@ -2120,30 +2278,29 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           name: "uri",
           description: gcli.lookup("appCacheValidateUriDesc"),
           defaultValue: null,
         }
       ]
     }],
     exec: function(args, context) {
       let utils;
-      let promise = context.createPromise();
+      let deferred = context.defer();
 
       if (args.uri) {
         utils = new AppCacheUtils(args.uri);
       } else {
-        let doc = context.environment.contentDocument;
-        utils = new AppCacheUtils(doc);
+        utils = new AppCacheUtils(context.environment.document);
       }
 
       utils.validateManifest().then(function(errors) {
-        promise.resolve([errors, utils.manifestURI || "-"]);
+        deferred.resolve([errors, utils.manifestURI || "-"]);
       });
 
-      return promise;
+      return deferred.promise;
     }
   });
 
   gcli.addCommand({
     name: 'appcache clear',
     description: gcli.lookup('appCacheClearDesc'),
     manual: gcli.lookup('appCacheClearManual'),
     exec: function(args, context) {
@@ -2233,44 +2390,36 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           type: "string",
           name: "search",
           description: gcli.lookup("appCacheListSearchDesc"),
           defaultValue: null,
         },
       ]
     }],
     exec: function(args, context) {
-      let doc = context.environment.contentDocument;
       let utils = new AppCacheUtils();
-
-      let entries = utils.listEntries(args.search);
-      return entries;
+      return utils.listEntries(args.search);
     }
   });
 
   gcli.addCommand({
     name: 'appcache viewentry',
     description: gcli.lookup('appCacheViewEntryDesc'),
     manual: gcli.lookup('appCacheViewEntryManual'),
     params: [
       {
         type: "string",
         name: "key",
         description: gcli.lookup("appCacheViewEntryKey"),
         defaultValue: null,
       }
     ],
     exec: function(args, context) {
-      let doc = context.environment.contentDocument;
       let utils = new AppCacheUtils();
-
-      let result = utils.viewEntry(args.key);
-      if (result) {
-        return result;
-      }
+      return utils.viewEntry(args.key);
     }
   });
 
   /**
    * Helper to find the 'data-command' attribute and call some action on it.
    * @see |updateCommand()| and |executeCommand()|
    */
   function withCommand(element, action) {
--- a/browser/devtools/commandline/gcli.jsm
+++ b/browser/devtools/commandline/gcli.jsm
@@ -83,36 +83,40 @@ var mozl10n = {};
     }
     catch (ex) {
       throw new Error("Failure in lookupFormat('" + name + "')");
     }
   };
 
 })(mozl10n);
 
-define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/command', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/selection', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/converters', 'gcli/ui/ffdisplay'], function(require, exports, module) {
+define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/selection', 'gcli/types/command', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/converters', 'gcli/ui/ffdisplay'], function(require, exports, module) {
 
   'use strict';
 
   // Internal startup process. Not exported
+  // The basic/selection are depended on by others so they must come first
   require('gcli/types/basic').startup();
+  require('gcli/types/selection').startup();
+
   require('gcli/types/command').startup();
   require('gcli/types/javascript').startup();
   require('gcli/types/node').startup();
   require('gcli/types/resource').startup();
   require('gcli/types/setting').startup();
-  require('gcli/types/selection').startup();
 
   require('gcli/settings').startup();
   require('gcli/ui/intro').startup();
   require('gcli/ui/focus').startup();
   require('gcli/ui/fields/basic').startup();
   require('gcli/ui/fields/javascript').startup();
   require('gcli/ui/fields/selection').startup();
 
+  require('gcli/commands/connect').startup();
+  require('gcli/commands/context').startup();
   require('gcli/commands/help').startup();
   require('gcli/commands/pref').startup();
 
   var Cc = Components.classes;
   var Ci = Components.interfaces;
   var prefSvc = "@mozilla.org/preferences-service;1";
   var prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
   var prefBranch = prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
@@ -182,31 +186,31 @@ var SelectionType = require('gcli/types/
 var BlankArgument = require('gcli/argument').BlankArgument;
 var ArrayArgument = require('gcli/argument').ArrayArgument;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(StringType);
-  types.registerType(NumberType);
-  types.registerType(BooleanType);
-  types.registerType(BlankType);
-  types.registerType(DelegateType);
-  types.registerType(ArrayType);
+  types.addType(StringType);
+  types.addType(NumberType);
+  types.addType(BooleanType);
+  types.addType(BlankType);
+  types.addType(DelegateType);
+  types.addType(ArrayType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(StringType);
-  types.unregisterType(NumberType);
-  types.unregisterType(BooleanType);
-  types.unregisterType(BlankType);
-  types.unregisterType(DelegateType);
-  types.unregisterType(ArrayType);
+  types.removeType(StringType);
+  types.removeType(NumberType);
+  types.removeType(BooleanType);
+  types.removeType(BlankType);
+  types.removeType(DelegateType);
+  types.removeType(ArrayType);
 };
 
 
 /**
  * 'string' the most basic string type that doesn't need to convert
  */
 function StringType(typeSpec) {
 }
@@ -472,16 +476,22 @@ DelegateType.prototype.getType = functio
 
 Object.defineProperty(DelegateType.prototype, 'isImportant', {
   get: function() {
     return this.delegateType().isImportant;
   },
   enumerable: true
 });
 
+/**
+ * DelegateType is designed to be inherited from, so DelegateField needs a way
+ * to check if something works like a delegate without using 'name'
+ */
+DelegateType.prototype.isDelegate = true;
+
 DelegateType.prototype.name = 'delegate';
 
 exports.DelegateType = DelegateType;
 
 
 /**
  * 'blank' is a type for use with DelegateType when we don't know yet.
  * It should not be used anywhere else.
@@ -512,17 +522,17 @@ function ArrayType(typeSpec) {
     console.error('Array.typeSpec is missing subtype. Assuming string.' +
         JSON.stringify(typeSpec));
     typeSpec.subtype = 'string';
   }
 
   Object.keys(typeSpec).forEach(function(key) {
     this[key] = typeSpec[key];
   }, this);
-  this.subtype = types.getType(this.subtype);
+  this.subtype = types.createType(this.subtype);
 }
 
 ArrayType.prototype = Object.create(Type.prototype);
 
 ArrayType.prototype.stringify = function(values, context) {
   if (values == null) {
     return '';
   }
@@ -960,21 +970,28 @@ exports.promiseEach = function(array, ac
 /**
  * Catching errors from promises isn't as simple as:
  *   promise.then(handler, console.error);
  * for a number of reasons:
  * - chrome's console doesn't have bound functions (why?)
  * - we don't get stack traces out from console.error(ex);
  */
 exports.errorHandler = function(ex) {
-  console.error(ex);
   if (ex instanceof Error) {
-    // Bizarrely the error message is part of the stack on node, but we'd
-    // rather have it twice than not at all
-    console.error(ex.stack);
+    // V8 weirdly includes the exception message in the stack
+    if (ex.stack.indexOf(ex.message) !== -1) {
+      console.error(ex.stack);
+    }
+    else {
+      console.error('' + ex);
+      console.error(ex.stack);
+    }
+  }
+  else {
+    console.error(ex);
   }
 };
 
 
 //------------------------------------------------------------------------------
 
 /**
  * XHTML namespace
@@ -1291,66 +1308,16 @@ exports.createUrlLookup = function(calli
         return callingModule.uri.substr(0, end) + '/' + path;
       }
 
       return filename + '/' + path;
     }
   };
 };
 
-/**
- * Helper to find the 'data-command' attribute and call some action on it.
- * @see |updateCommand()| and |executeCommand()|
- */
-function withCommand(element, action) {
-  var command = element.getAttribute('data-command');
-  if (!command) {
-    command = element.querySelector('*[data-command]')
-            .getAttribute('data-command');
-  }
-
-  if (command) {
-    action(command);
-  }
-  else {
-    console.warn('Missing data-command for ' + util.findCssSelector(element));
-  }
-}
-
-/**
- * Update the requisition to contain the text of the clicked element
- * @param element The clicked element, containing either a data-command
- * attribute directly or in a nested element, from which we get the command
- * to be executed.
- * @param context Either a Requisition or an ExecutionContext or another object
- * that contains an |update()| function that follows a similar contract.
- */
-exports.updateCommand = function(element, context) {
-  withCommand(element, function(command) {
-    context.update(command);
-  });
-};
-
-/**
- * Execute the text contained in the element that was clicked
- * @param element The clicked element, containing either a data-command
- * attribute directly or in a nested element, from which we get the command
- * to be executed.
- * @param context Either a Requisition or an ExecutionContext or another object
- * that contains an |update()| function that follows a similar contract.
- */
-exports.executeCommand = function(element, context) {
-  withCommand(element, function(command) {
-    context.exec({
-      visible: true,
-      typed: command
-    });
-  });
-};
-
 
 //------------------------------------------------------------------------------
 
 /**
  * Keyboard handling is a mess. http://unixpapa.com/js/key.html
  * It would be good to use DOM L3 Keyboard events,
  * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
  * however only Webkit supports them, and there isn't a shim on Monernizr:
@@ -2017,80 +1984,103 @@ exports.getTypeNames = function() {
  * Add a new type to the list available to the system.
  * You can pass 2 things to this function - either an instance of Type, in
  * which case we return this instance when #getType() is called with a 'name'
  * that matches type.name.
  * Also you can pass in a constructor (i.e. function) in which case when
  * #getType() is called with a 'name' that matches Type.prototype.name we will
  * pass the typeSpec into this constructor.
  */
-exports.registerType = function(type) {
+exports.addType = function(type) {
   if (typeof type === 'object') {
+    if (!type.name) {
+      throw new Error('All registered types must have a name');
+    }
+
     if (type instanceof Type) {
-      if (!type.name) {
-        throw new Error('All registered types must have a name');
-      }
       registeredTypes[type.name] = type;
     }
     else {
-      throw new Error('Can\'t registerType using: ' + type);
+      if (!type.parent) {
+        throw new Error('\'parent\' property required for object declarations');
+      }
+      var name = type.name;
+      var parent = type.parent;
+      type.name = parent;
+      delete type.parent;
+
+      registeredTypes[name] = exports.createType(type);
+
+      type.name = name;
+      type.parent = parent;
     }
   }
   else if (typeof type === 'function') {
     if (!type.prototype.name) {
       throw new Error('All registered types must have a name');
     }
     registeredTypes[type.prototype.name] = type;
   }
   else {
     throw new Error('Unknown type: ' + type);
   }
 };
 
-exports.registerTypes = function registerTypes(newTypes) {
-  Object.keys(newTypes).forEach(function(name) {
-    var type = newTypes[name];
-    type.name = name;
-    newTypes.registerType(type);
-  });
-};
-
 /**
  * Remove a type from the list available to the system
  */
-exports.deregisterType = function(type) {
+exports.removeType = function(type) {
   delete registeredTypes[type.name];
 };
 
 /**
- * Find a type, previously registered using #registerType()
- */
-exports.getType = function(typeSpec) {
-  var type;
+ * Find a type, previously registered using #addType()
+ */
+exports.createType = function(typeSpec) {
   if (typeof typeSpec === 'string') {
-    type = registeredTypes[typeSpec];
-    if (typeof type === 'function') {
-      type = new type({});
-    }
-    return type;
-  }
-
-  if (typeof typeSpec === 'object') {
-    if (!typeSpec.name) {
-      throw new Error('Missing \'name\' member to typeSpec');
-    }
-
-    type = registeredTypes[typeSpec.name];
-    if (typeof type === 'function') {
-      type = new type(typeSpec);
-    }
-    return type;
-  }
-
-  throw new Error('Can\'t extract type from ' + typeSpec);
+    typeSpec = { name: typeSpec };
+  }
+
+  if (typeof typeSpec !== 'object') {
+    throw new Error('Can\'t extract type from ' + typeSpec);
+  }
+
+  if (!typeSpec.name) {
+    throw new Error('Missing \'name\' member to typeSpec');
+  }
+
+  var newType;
+  var type = registeredTypes[typeSpec.name];
+
+  if (!type) {
+    console.error('Known types: ' + Object.keys(registeredTypes).join(', '));
+    throw new Error('Unknown type: \'' + typeSpec.name + '\'');
+  }
+
+  if (typeof type === 'function') {
+    newType = new type(typeSpec);
+  }
+  else {
+    // Shallow clone 'type'
+    newType = {};
+    for (var key in type) {
+      newType[key] = type[key];
+    }
+
+    // Copy the properties of typeSpec onto the new type
+    for (var key in typeSpec) {
+      newType[key] = typeSpec[key];
+    }
+
+    if (typeof newType.constructor === 'function') {
+      newType.constructor();
+    }
+  }
+
+  return newType;
 };
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -2718,21 +2708,21 @@ var Conversion = require('gcli/types').C
 var spell = require('gcli/types/spell');
 var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(SelectionType);
+  types.addType(SelectionType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(SelectionType);
+  types.removeType(SelectionType);
 };
 
 
 /**
  * A selection allows the user to pick a value from known set of options.
  * An option is made up of a name (which is what the user types) and a value
  * (which is passed to exec)
  * @param typeSpec Object containing properties that describe how this
@@ -3057,16 +3047,22 @@ SelectionType.prototype._findValue = fun
     if (pair.value === value) {
       index = i;
       break;
     }
   }
   return index;
 };
 
+/**
+ * SelectionType is designed to be inherited from, so SelectionField needs a way
+ * to check if something works like a selection without using 'name'
+ */
+SelectionType.prototype.isSelection = true;
+
 SelectionType.prototype.name = 'selection';
 
 exports.SelectionType = SelectionType;
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
@@ -3217,23 +3213,23 @@ var SelectionType = require('gcli/types/
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(CommandType);
-  types.registerType(ParamType);
+  types.addType(CommandType);
+  types.addType(ParamType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(CommandType);
-  types.unregisterType(ParamType);
+  types.removeType(CommandType);
+  types.removeType(ParamType);
 };
 
 
 /**
  * Select from the available commands.
  * This is very similar to a SelectionType, however the level of hackery in
  * SelectionType to make it handle Commands correctly was to high, so we
  * simplified.
@@ -3361,29 +3357,26 @@ CommandType.prototype.parse = function(a
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/canon', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types', 'gcli/types/basic', 'gcli/types/selection'], function(require, exports, module) {
+define('gcli/canon', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types'], function(require, exports, module) {
 
 'use strict';
-var canon = exports;
 
 var Promise = require('util/promise');
 var util = require('util/util');
 var l10n = require('util/l10n');
 
 var types = require('gcli/types');
 var Status = require('gcli/types').Status;
-var BooleanType = require('gcli/types/basic').BooleanType;
-var SelectionType = require('gcli/types/selection').SelectionType;
 
 /**
  * Implement the localization algorithm for any documentation objects (i.e.
  * description and manual) in a command.
  * @param data The data assigned to a description or manual property
  * @param onUndefined If data == null, should we return the data untouched or
  * lookup a 'we don't know' key in it's place.
  */
@@ -3495,17 +3488,17 @@ function Command(commandSpec) {
         }
       }, this);
 
       usingGroups = true;
     }
   }, this);
 }
 
-canon.Command = Command;
+exports.Command = Command;
 
 
 /**
  * A wrapper for a paramSpec so we can sort out shortened versions names for
  * option switches
  */
 function Parameter(paramSpec, command, groupName) {
   this.command = command || { name: 'unnamed' };
@@ -3528,26 +3521,26 @@ function Parameter(paramSpec, command, g
   }
 
   if (!this.name) {
     throw new Error('In ' + this.command.name +
                     ': all params must have a name');
   }
 
   var typeSpec = this.type;
-  this.type = types.getType(typeSpec);
+  this.type = types.createType(typeSpec);
   if (this.type == null) {
     console.error('Known types: ' + types.getTypeNames().join(', '));
     throw new Error('In ' + this.command.name + '/' + this.name +
                     ': can\'t find type for: ' + JSON.stringify(typeSpec));
   }
 
   // boolean parameters have an implicit defaultValue:false, which should
   // not be changed. See the docs.
-  if (this.type instanceof BooleanType &&
+  if (this.type.name === 'boolean' &&
       this.paramSpec.defaultValue !== undefined) {
     throw new Error('In ' + this.command.name + '/' + this.name +
                     ': boolean parameters can not have a defaultValue.' +
                     ' Ignoring');
   }
 
   // Check the defaultValue for validity.
   // Both undefined and null get a pass on this test. undefined is used when
@@ -3571,17 +3564,17 @@ function Parameter(paramSpec, command, g
     catch (ex) {
       throw new Error('In ' + this.command.name + '/' + this.name + ': ' + ex);
     }
   }
 
   // All parameters that can only be set via a named parameter must have a
   // non-undefined default value
   if (!this.isPositionalAllowed && this.paramSpec.defaultValue === undefined &&
-      this.type.getBlank == null && !(this.type instanceof BooleanType)) {
+      this.type.getBlank == null && !(this.type.name === 'boolean')) {
     throw new Error('In ' + this.command.name + '/' + this.name +
                     ': Missing defaultValue for optional parameter.');
   }
 }
 
 /**
  * type.getBlank can be expensive, so we delay execution where we can
  */
@@ -3668,140 +3661,235 @@ Object.defineProperty(Parameter.prototyp
  */
 Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', {
   get: function() {
     return this.groupName == null;
   },
   enumerable: true
 });
 
-canon.Parameter = Parameter;
-
-
-/**
- * A lookup hash of our registered commands
- */
-var commands = {};
-
-/**
- * A sorted list of command names, we regularly want them in order, so pre-sort
- */
-var commandNames = [];
-
-/**
- * A lookup of the original commandSpecs by command name
- */
-var commandSpecs = {};
+exports.Parameter = Parameter;
+
+
+/**
+ * A canon is a store for a list of commands
+ */
+function Canon() {
+  // A lookup hash of our registered commands
+  this._commands = {};
+  // A sorted list of command names, we regularly want them in order, so pre-sort
+  this._commandNames = [];
+  // A lookup of the original commandSpecs by command name
+  this._commandSpecs = {};
+
+  // Enable people to be notified of changes to the list of commands
+  this.onCanonChange = util.createEvent('canon.onCanonChange');
+}
 
 /**
  * Add a command to the canon of known commands.
  * This function is exposed to the outside world (via gcli/index). It is
  * documented in docs/index.md for all the world to see.
  * @param commandSpec The command and its metadata.
  * @return The new command
  */
-canon.addCommand = function addCommand(commandSpec) {
-  if (commands[commandSpec.name] != null) {
+Canon.prototype.addCommand = function(commandSpec) {
+  if (this._commands[commandSpec.name] != null) {
     // Roughly canon.removeCommand() without the event call, which we do later
-    delete commands[commandSpec.name];
-    commandNames = commandNames.filter(function(test) {
+    delete this._commands[commandSpec.name];
+    this._commandNames = this._commandNames.filter(function(test) {
       return test !== commandSpec.name;
     });
   }
 
   var command = new Command(commandSpec);
-  commands[commandSpec.name] = command;
-  commandNames.push(commandSpec.name);
-  commandNames.sort();
-
-  commandSpecs[commandSpec.name] = commandSpec;
-
-  canon.onCanonChange();
+  this._commands[commandSpec.name] = command;
+  this._commandNames.push(commandSpec.name);
+  this._commandNames.sort();
+
+  this._commandSpecs[commandSpec.name] = commandSpec;
+
+  this.onCanonChange();
   return command;
 };
 
 /**
  * Remove an individual command. The opposite of #addCommand().
  * Removing a non-existent command is a no-op.
  * @param commandOrName Either a command name or the command itself.
  * @return true if a command was removed, false otherwise.
  */
-canon.removeCommand = function removeCommand(commandOrName) {
+Canon.prototype.removeCommand = function(commandOrName) {
   var name = typeof commandOrName === 'string' ?
           commandOrName :
           commandOrName.name;
 
-  if (!commands[name]) {
+  if (!this._commands[name]) {
     return false;
   }
 
   // See start of canon.addCommand if changing this code
-  delete commands[name];
-  delete commandSpecs[name];
-  commandNames = commandNames.filter(function(test) {
+  delete this._commands[name];
+  delete this._commandSpecs[name];
+  this._commandNames = this._commandNames.filter(function(test) {
     return test !== name;
   });
 
-  canon.onCanonChange();
+  this.onCanonChange();
   return true;
 };
 
 /**
  * Retrieve a command by name
  * @param name The name of the command to retrieve
  */
-canon.getCommand = function getCommand(name) {
+Canon.prototype.getCommand = function(name) {
   // '|| undefined' is to silence 'reference to undefined property' warnings
-  return commands[name] || undefined;
+  return this._commands[name] || undefined;
 };
 
 /**
  * Get an array of all the registered commands.
  */
-canon.getCommands = function getCommands() {
-  // return Object.values(commands);
-  return Object.keys(commands).map(function(name) {
-    return commands[name];
+Canon.prototype.getCommands = function() {
+  return Object.keys(this._commands).map(function(name) {
+    return this._commands[name];
   }, this);
 };
 
 /**
  * Get an array containing the names of the registered commands.
  */
-canon.getCommandNames = function getCommandNames() {
-  return commandNames.slice(0);
+Canon.prototype.getCommandNames = function() {
+  return this._commandNames.slice(0);
 };
 
 /**
  * Get access to the stored commandMetaDatas (i.e. before they were made into
  * instances of Command/Parameters) so we can remote them.
  */
-canon.getCommandSpecs = function getCommandSpecs() {
-  return commandSpecs;
-};
-
-/**
- * Enable people to be notified of changes to the list of commands
- */
-canon.onCanonChange = util.createEvent('canon.onCanonChange');
+Canon.prototype.getCommandSpecs = function() {
+  var specs = {};
+
+  Object.keys(this._commandSpecs).forEach(function(name) {
+    var spec = this._commandSpecs[name];
+    if (spec.exec == null) {
+      spec.isParent = true;
+    }
+    specs[name] = spec;
+  }.bind(this));
+
+  return specs;
+};
+
+/**
+ * Add a set of commands that are executed somewhere else.
+ * @param prefix The name prefix that we assign to all command names
+ * @param commandSpecs Presumably as obtained from getCommandSpecs on remote
+ * @param remoter Function to call on exec of a new remote command. This is
+ * defined just like an exec function (i.e. that takes args/context as params
+ * and returns a promise) with one extra feature, that the context includes a
+ * 'commandName' property that contains the original command name.
+ * @param to URL-like string that describes where the commands are executed.
+ * This is to complete the parent command description.
+ */
+Canon.prototype.addProxyCommands = function(prefix, commandSpecs, remoter, to) {
+  var names = Object.keys(commandSpecs);
+
+  if (this._commands[prefix] != null) {
+    throw new Error(l10n.lookupFormat('canonProxyExists', [ prefix ]));
+  }
+
+  // We need to add the parent command so all the commands from the other
+  // system have a parent
+  this.addCommand({
+    name: prefix,
+    isProxy: true,
+    description: l10n.lookupFormat('canonProxyDesc', [ to ]),
+    manual: l10n.lookupFormat('canonProxyManual', [ to ])
+  });
+
+  names.forEach(function(name) {
+    var commandSpec = commandSpecs[name];
+
+    if (!commandSpec.isParent) {
+      commandSpec.exec = function(args, context) {
+        context.commandName = name;
+        return remoter(args, context);
+      }.bind(this);
+    }
+
+    commandSpec.name = prefix + ' ' + commandSpec.name;
+    commandSpec.isProxy = true;
+    this.addCommand(commandSpec);
+  }.bind(this));
+};
+
+/**
+ * Add a set of commands that are executed somewhere else.
+ * @param prefix The name prefix that we assign to all command names
+ * @param commandSpecs Presumably as obtained from getCommandSpecs on remote
+ * @param remoter Function to call on exec of a new remote command. This is
+ * defined just like an exec function (i.e. that takes args/context as params
+ * and returns a promise) with one extra feature, that the context includes a
+ * 'commandName' property that contains the original command name.
+ * @param to URL-like string that describes where the commands are executed.
+ * This is to complete the parent command description.
+ */
+Canon.prototype.removeProxyCommands = function(prefix) {
+  var toRemove = [];
+  Object.keys(this._commandSpecs).forEach(function(name) {
+    if (name.indexOf(prefix) === 0) {
+      toRemove.push(name);
+    }
+  }.bind(this));
+
+  var removed = [];
+  toRemove.forEach(function(name) {
+    var command = this.getCommand(name);
+    if (command.isProxy) {
+      this.removeCommand(name);
+      removed.push(name);
+    }
+    else {
+      console.error('Skipping removal of \'' + name +
+                    '\' because it is not a proxy command.');
+    }
+  }.bind(this));
+
+  return removed;
+};
+
+var canon = new Canon();
+
+exports.Canon = Canon;
+exports.addCommand = canon.addCommand.bind(canon);
+exports.removeCommand = canon.removeCommand.bind(canon);
+exports.onCanonChange = canon.onCanonChange;
+exports.getCommands = canon.getCommands.bind(canon);
+exports.getCommand = canon.getCommand.bind(canon);
+exports.getCommandNames = canon.getCommandNames.bind(canon);
+exports.getCommandSpecs = canon.getCommandSpecs.bind(canon);
+exports.addProxyCommands = canon.addProxyCommands.bind(canon);
+exports.removeProxyCommands = canon.removeProxyCommands.bind(canon);
 
 /**
  * CommandOutputManager stores the output objects generated by executed
  * commands.
  *
  * CommandOutputManager is exposed to the the outside world and could (but
  * shouldn't) be used before gcli.startup() has been called.
  * This could should be defensive to that where possible, and we should
  * certainly document if the use of it or similar will fail if used too soon.
  */
 function CommandOutputManager() {
   this.onOutput = util.createEvent('CommandOutputManager.onOutput');
 }
 
-canon.CommandOutputManager = CommandOutputManager;
+exports.CommandOutputManager = CommandOutputManager;
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -3828,21 +3916,21 @@ var Conversion = types.Conversion;
 var Type = types.Type;
 var Status = types.Status;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(JavascriptType);
+  types.addType(JavascriptType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(JavascriptType);
+  types.removeType(JavascriptType);
 };
 
 /**
  * The object against which we complete, which is usually 'window' if it exists
  * but could be something else in non-web-content environments.
  */
 var globalObject = undefined;
 if (typeof window !== 'undefined') {
@@ -4393,23 +4481,23 @@ var Status = require('gcli/types').Statu
 var Conversion = require('gcli/types').Conversion;
 var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(NodeType);
-  types.registerType(NodeListType);
+  types.addType(NodeType);
+  types.addType(NodeListType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(NodeType);
-  types.unregisterType(NodeListType);
+  types.removeType(NodeType);
+  types.removeType(NodeListType);
 };
 
 /**
  * The object against which we complete, which is usually 'window' if it exists
  * but could be something else in non-web-content environments.
  */
 var doc = undefined;
 if (typeof document !== 'undefined') {
@@ -4635,21 +4723,21 @@ var Promise = require('util/promise');
 var types = require('gcli/types');
 var SelectionType = require('gcli/types/selection').SelectionType;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(ResourceType);
+  types.addType(ResourceType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(ResourceType);
+  types.removeType(ResourceType);
   exports.clearResourceCache();
 };
 
 exports.clearResourceCache = function() {
   ResourceCache.clear();
 };
 
 /**
@@ -4939,83 +5027,76 @@ var ResourceCache = {
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types', 'gcli/types/selection', 'gcli/types/basic'], function(require, exports, module) {
+define('gcli/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types'], function(require, exports, module) {
 
 'use strict';
 
 var settings = require('gcli/settings');
 var types = require('gcli/types');
-var SelectionType = require('gcli/types/selection').SelectionType;
-var DelegateType = require('gcli/types/basic').DelegateType;
-
+
+/**
+ * A type for selecting a known setting
+ */
+var settingType = {
+  constructor: function() {
+    settings.onChange.add(function(ev) {
+      this.clearCache();
+    }, this);
+  },
+  name: 'setting',
+  parent: 'selection',
+  cacheable: true,
+  lookup: function() {
+    return settings.getAll().map(function(setting) {
+      return { name: setting.name, value: setting };
+    });
+  }
+};
+
+/**
+ * A type for entering the value of a known setting
+ * Customizations:
+ * - settingParamName The name of the setting parameter so we can customize the
+ *   type that we are expecting to read
+ */
+var settingValueType = {
+  name: 'settingValue',
+  parent: 'delegate',
+  settingParamName: 'setting',
+  delegateType: function(context) {
+    if (context != null) {
+      var setting = context.getArgsObject()[this.settingParamName];
+      if (setting != null) {
+        return setting.type;
+      }
+    }
+
+    return types.createType('blank');
+  }
+};
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
-  types.registerType(SettingType);
-  types.registerType(SettingValueType);
+  types.addType(settingType);
+  types.addType(settingValueType);
 };
 
 exports.shutdown = function() {
-  types.unregisterType(SettingType);
-  types.unregisterType(SettingValueType);
-};
-
-/**
- * A type for selecting a known setting
- */
-function SettingType(typeSpec) {
-  settings.onChange.add(function(ev) {
-    this.clearCache();
-  }, this);
-}
-
-SettingType.prototype = new SelectionType({ cacheable: true });
-
-SettingType.prototype.lookup = function() {
-  return settings.getAll().map(function(setting) {
-    return { name: setting.name, value: setting };
-  });
-};
-
-SettingType.prototype.name = 'setting';
-
-
-/**
- * A type for entering the value of a known setting
- * @param typeSpec Customization object, can contain the following:
- * - settingParamName The name of the setting parameter so we can customize the
- *   type that we are expecting to read
- */
-function SettingValueType(typeSpec) {
-  this.settingParamName = typeSpec.settingParamName || 'setting';
-}
-
-SettingValueType.prototype = Object.create(DelegateType.prototype);
-
-SettingValueType.prototype.delegateType = function(context) {
-  if (context != null) {
-    var setting = context.getArgsObject()[this.settingParamName];
-    if (setting != null) {
-      return setting.type;
-    }
-  }
-
-  return types.getType('blank');
-};
-
-SettingValueType.prototype.name = 'settingValue';
+  types.removeType(settingType);
+  types.removeType(settingValueType);
+};
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -5088,23 +5169,23 @@ function Setting(prefSpec) {
 
 /**
  * What type is this property: boolean/integer/string?
  */
 Object.defineProperty(Setting.prototype, 'type', {
   get: function() {
     switch (imports.prefBranch.getPrefType(this.name)) {
       case imports.prefBranch.PREF_BOOL:
-        return types.getType('boolean');
+        return types.createType('boolean');
 
       case imports.prefBranch.PREF_INT:
-        return types.getType('number');
+        return types.createType('number');
 
       case imports.prefBranch.PREF_STRING:
-        return types.getType('string');
+        return types.createType('string');
 
       default:
         throw new Error('Unknown type for ' + this.name);
     }
   },
   enumerable: true
 });
 
@@ -5318,21 +5399,20 @@ exports.removeSetting = function() { };
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/intro', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/settings', 'gcli/ui/view', 'gcli/cli', 'text!gcli/ui/intro.html'], function(require, exports, module) {
+define('gcli/ui/intro', ['require', 'exports', 'module' , 'util/l10n', 'gcli/settings', 'gcli/ui/view', 'gcli/cli', 'text!gcli/ui/intro.html'], function(require, exports, module) {
 
 'use strict';
 
-var util = require('util/util');
 var l10n = require('util/l10n');
 var settings = require('gcli/settings');
 var view = require('gcli/ui/view');
 var Output = require('gcli/cli').Output;
 
 /**
  * Record if the user has clicked on 'Got It!'
  */
@@ -5354,44 +5434,44 @@ exports.startup = function() {
 exports.shutdown = function() {
   settings.removeSetting(hideIntroSettingSpec);
   hideIntro = undefined;
 };
 
 /**
  * Called when the UI is ready to add a welcome message to the output
  */
-exports.maybeShowIntro = function(commandOutputManager, context) {
+exports.maybeShowIntro = function(commandOutputManager, conversionContext) {
   if (hideIntro.value) {
     return;
   }
 
   var output = new Output();
   output.type = 'view';
   commandOutputManager.onOutput({ output: output });
 
-  var viewData = this.createView(context, output);
-
-  output.complete(viewData);
+  var viewData = this.createView(conversionContext, output);
+
+  output.complete({ isTypedData: true, type: 'view', data: viewData });
 };
 
 /**
  * Called when the UI is ready to add a welcome message to the output
  */
-exports.createView = function(context, output) {
+exports.createView = function(conversionContext, output) {
   return view.createView({
     html: require('text!gcli/ui/intro.html'),
     options: { stack: 'intro.html' },
     data: {
       l10n: l10n.propertyLookup,
       onclick: function(ev) {
-        util.updateCommand(ev.currentTarget, context);
+        conversionContext.update(ev.currentTarget);
       },
       ondblclick: function(ev) {
-        util.executeCommand(ev.currentTarget, context);
+        conversionContext.updateExec(ev.currentTarget);
       },
       showHideButton: (output != null),
       onGotIt: function(ev) {
         hideIntro.value = true;
         output.onClose();
       }
     }
   });
@@ -5525,34 +5605,30 @@ define('util/domtemplate', ['require', '
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/cli', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/ui/view', 'gcli/canon', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) {
+define('gcli/cli', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/ui/view', 'gcli/canon', 'gcli/types', 'gcli/argument'], function(require, exports, module) {
 
 'use strict';
 
 var Promise = require('util/promise');
 var util = require('util/util');
 var l10n = require('util/l10n');
 
 var view = require('gcli/ui/view');
 var canon = require('gcli/canon');
 var CommandOutputManager = require('gcli/canon').CommandOutputManager;
 
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
-var ArrayType = require('gcli/types/basic').ArrayType;
-var StringType = require('gcli/types/basic').StringType;
-var BooleanType = require('gcli/types/basic').BooleanType;
-var NumberType = require('gcli/types/basic').NumberType;
 
 var Argument = require('gcli/argument').Argument;
 var ArrayArgument = require('gcli/argument').ArrayArgument;
 var NamedArgument = require('gcli/argument').NamedArgument;
 var TrueNamedArgument = require('gcli/argument').TrueNamedArgument;
 var MergedArgument = require('gcli/argument').MergedArgument;
 var ScriptArgument = require('gcli/argument').ScriptArgument;
 
@@ -5840,27 +5916,28 @@ function UnassignedAssignment(requisitio
       requisition: requisition,
       isIncompleteName: (arg.text.charAt(0) === '-')
     }
   });
   this.paramIndex = -1;
   this.onAssignmentChange = util.createEvent('UnassignedAssignment.onAssignmentChange');
 
   // synchronize is ok because we can be sure that param type is synchronous
-  var parsed = this.param.type.parse(arg, requisition.context);
+  var parsed = this.param.type.parse(arg, requisition.executionContext);
   this.conversion = util.synchronize(parsed);
   this.conversion.assign(this);
 }
 
 UnassignedAssignment.prototype = Object.create(Assignment.prototype);
 
 UnassignedAssignment.prototype.getStatus = function(arg) {
   return this.conversion.getStatus();
 };
 
+exports.logErrors = true;
 
 /**
  * A Requisition collects the information needed to execute a command.
  *
  * (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition)
  * This term is used because carries the notion of a work-flow, or process to
  * getting the information to execute a command correct.
  * There is little point in a requisition for parameter-less commands because
@@ -5892,17 +5969,16 @@ function Requisition(environment, doc, c
   if (this.document == null) {
     try {
       this.document = document;
     }
     catch (ex) {
       // Ignore
     }
   }
-  this.context = exports.createExecutionContext(this);
 
   this.commandOutputManager = commandOutputManager || new CommandOutputManager();
 
   // The command that we are about to execute.
   // @see setCommandConversion()
   this.commandAssignment = new CommandAssignment();
   var promise = this.setAssignment(this.commandAssignment, null,
                                    { skipArgUpdate: true });
@@ -5924,16 +6000,20 @@ function Requisition(environment, doc, c
 
   // Used to store cli arguments that were not assigned to parameters
   this._unassigned = [];
 
   // Temporarily set this to true to prevent _assignmentChanged resetting
   // argument positions
   this._structuralChangeInProgress = false;
 
+  // We can set a prefix to typed commands to make it easier to focus on
+  // Allowing us to type "add -a; commit" in place of "git add -a; git commit"
+  this.prefix = '';
+
   this.commandAssignment.onAssignmentChange.add(this._commandAssignmentChanged, this);
   this.commandAssignment.onAssignmentChange.add(this._assignmentChanged, this);
 
   this.onAssignmentChange = util.createEvent('Requisition.onAssignmentChange');
   this.onTextChange = util.createEvent('Requisition.onTextChange');
 }
 
 /**
@@ -5942,16 +6022,120 @@ function Requisition(environment, doc, c
 Requisition.prototype.destroy = function() {
   this.commandAssignment.onAssignmentChange.remove(this._commandAssignmentChanged, this);
   this.commandAssignment.onAssignmentChange.remove(this._assignmentChanged, this);
 
   delete this.document;
   delete this.environment;
 };
 
+var legacy = false;
+
+/**
+ * Functions and data related to the execution of a command
+ */
+Object.defineProperty(Requisition.prototype, 'executionContext', {
+  get: function() {
+    if (this._executionContext == null) {
+      this._executionContext = {
+        defer: function() {
+          return Promise.defer();
+        },
+        typedData: function(type, data) {
+          return {
+            isTypedData: true,
+            data: data,
+            type: type
+          };
+        },
+        getArgsObject: this.getArgsObject.bind(this)
+      };
+
+      // Alias requisition so we're clear about what's what
+      var requisition = this;
+      Object.defineProperty(this._executionContext, 'typed', {
+        get: function() { return requisition.toString(); },
+        enumerable: true
+      });
+      Object.defineProperty(this._executionContext, 'environment', {
+        get: function() { return requisition.environment; },
+        enumerable: true
+      });
+
+      /**
+       * This is a temporary property that will change and/or be removed.
+       * Do not use it
+       */
+      Object.defineProperty(this._executionContext, '__dlhjshfw', {
+        get: function() { return requisition; },
+        enumerable: false
+      });
+
+      if (legacy) {
+        this._executionContext.createView = view.createView;
+        this._executionContext.exec = this.exec.bind(this);
+        this._executionContext.update = this.update.bind(this);
+        this._executionContext.updateExec = this.updateExec.bind(this);
+
+        Object.defineProperty(this._executionContext, 'document', {
+          get: function() { return requisition.document; },
+          enumerable: true
+        });
+      }
+    }
+
+    return this._executionContext;
+  },
+  enumerable: true
+});
+
+/**
+ * Functions and data related to the conversion of the output of a command
+ */
+Object.defineProperty(Requisition.prototype, 'conversionContext', {
+  get: function() {
+    if (this._conversionContext == null) {
+      this._conversionContext = {
+        defer: function() {
+          return Promise.defer();
+        },
+
+        createView: view.createView,
+        exec: this.exec.bind(this),
+        update: this.update.bind(this),
+        updateExec: this.updateExec.bind(this)
+      };
+
+      // Alias requisition so we're clear about what's what
+      var requisition = this;
+
+      Object.defineProperty(this._conversionContext, 'document', {
+        get: function() { return requisition.document; },
+        enumerable: true
+      });
+      Object.defineProperty(this._conversionContext, 'environment', {
+        get: function() { return requisition.environment; },
+        enumerable: true
+      });
+
+      /**
+       * This is a temporary property that will change and/or be removed.
+       * Do not use it
+       */
+      Object.defineProperty(this._conversionContext, '__dlhjshfw', {
+        get: function() { return requisition; },
+        enumerable: false
+      });
+    }
+
+    return this._conversionContext;
+  },
+  enumerable: true
+});
+
 /**
  * When any assignment changes, we might need to update the _args array to
  * match and inform people of changes to the typed input text.
  */
 Requisition.prototype._assignmentChanged = function(ev) {
   // Don't report an event if the value is unchanged
   if (ev.oldConversion != null &&
       ev.conversion.valueEquals(ev.oldConversion)) {
@@ -6192,17 +6376,17 @@ Requisition.prototype.setAssignment = fu
 
   if (arg == null) {
     setAssignmentInternal(assignment.param.type.getBlank());
   }
   else if (typeof arg.getStatus === 'function') {
     setAssignmentInternal(arg);
   }
   else {
-    var parsed = assignment.param.type.parse(arg, this.context);
+    var parsed = assignment.param.type.parse(arg, this.executionContext);
     return parsed.then(function(conversion) {
       setAssignmentInternal(conversion);
     }.bind(this));
   }
 
   return Promise.resolve(undefined);
 };
 
@@ -6323,33 +6507,35 @@ Requisition.prototype._addSpace = functi
   }
 };
 
 /**
  * Replace the current value with the lower value if such a concept exists.
  */
 Requisition.prototype.decrement = function(assignment) {
   var replacement = assignment.param.type.decrement(assignment.conversion.value,
-                                                    this.context);
+                                                    this.executionContext);
   if (replacement != null) {
-    var str = assignment.param.type.stringify(replacement, this.context);
+    var str = assignment.param.type.stringify(replacement,
+                                              this.executionContext);
     var arg = assignment.conversion.arg.beget({ text: str });
     var promise = this.setAssignment(assignment, arg);
     util.synchronize(promise);
   }
 };
 
 /**
  * Replace the current value with the higher value if such a concept exists.
  */
 Requisition.prototype.increment = function(assignment) {
   var replacement = assignment.param.type.increment(assignment.conversion.value,
-                                                    this.context);
+                                                    this.executionContext);
   if (replacement != null) {
-    var str = assignment.param.type.stringify(replacement, this.context);
+    var str = assignment.param.type.stringify(replacement,
+                                              this.executionContext);
     var arg = assignment.conversion.arg.beget({ text: str });
     var promise = this.setAssignment(assignment, arg);
     util.synchronize(promise);
   }
 };
 
 /**
  * Extract a canonical version of the input
@@ -6365,17 +6551,17 @@ Requisition.prototype.toCanonicalString 
   Object.keys(this._assignments).forEach(function(name) {
     var assignment = this._assignments[name];
     var type = assignment.param.type;
     // Bug 664377: This will cause problems if there is a non-default value
     // after a default value. Also we need to decide when to use
     // named parameters in place of positional params. Both can wait.
     if (assignment.value !== assignment.param.defaultValue) {
       line.push(' ');
-      line.push(type.stringify(assignment.value, this.context));
+      line.push(type.stringify(assignment.value, this.executionContext));
     }
   }, this);
 
   // Canonically, if we've opened with a { then we should have a } to close
   if (cmd === '{') {
     if (this.getAssignment(0).arg.suffix.indexOf('}') === -1) {
       line.push(' }');
     }
@@ -6390,17 +6576,17 @@ Requisition.prototype.toCanonicalString 
  * to display this typed input. It's a bit like toString on steroids.
  * <p>
  * The returned object has the following members:<ul>
  * <li>character: The character to which this arg trace refers.
  * <li>arg: The Argument to which this character is assigned.
  * <li>part: One of ['prefix'|'text'|suffix'] - how was this char understood
  * </ul>
  * <p>
- * The Argument objects are as output from #_tokenize() rather than as applied
+ * The Argument objects are as output from #tokenize() rather than as applied
  * to Assignments by #_assign() (i.e. they are not instances of NamedArgument,
  * ArrayArgument, etc).
  * <p>
  * To get at the arguments applied to the assignments simply call
  * <tt>arg.assignment.arg</tt>. If <tt>arg.assignment.arg !== arg</tt> then
  * the arg applied to the assignment will contain the original arg.
  * See #_assign() for details.
  */
@@ -6660,30 +6846,33 @@ Requisition.prototype.exec = function(op
 
   this.commandOutputManager.onOutput({ output: output });
 
   var onDone = function(data) {
     output.complete(data, false);
   };
 
   var onError = function(ex) {
-    output.complete({
+    if (exports.logErrors) {
+      util.errorHandler(ex);
+    }
+
+    var data = ex.isTypedData ? ex : {
       isTypedData: true,
       data: ex,
-      type: "exception"
-    }, true);
+      type: 'error'
+    };
+    output.complete(data, true);
   };
 
   try {
-    var reply = command.exec(args, this.context);
-
-    this._then(reply, onDone, onError);
+    var reply = command.exec(args, this.executionContext);
+    Promise.resolve(reply).then(onDone, onError);
   }
   catch (ex) {
-    console.error(ex);
     onError(ex);
   }
 
   this.clear();
   return output;
 };
 
 /**
@@ -6703,60 +6892,54 @@ Requisition.prototype.updateExec = funct
  */
 Requisition.prototype.clear = function() {
   this._structuralChangeInProgress = true;
 
   var arg = new Argument('', '', '');
   this._args = [ arg ];
 
   var commandType = this.commandAssignment.param.type;
-  var conversion = util.synchronize(commandType.parse(arg, this.context));
-  this.setAssignment(this.commandAssignment, conversion,
+  var promise = commandType.parse(arg, this.executionContext);
+  this.setAssignment(this.commandAssignment,
+                     util.synchronize(promise),
                      { skipArgUpdate: true });
 
   this._structuralChangeInProgress = false;
   this.onTextChange();
 };
 
 /**
- * Different types of promise have different ways of doing 'then'. This is a
- * catch-all so we can ignore the differences. It also handles concrete values
- * and calls onDone directly if thing is not a promise.
- * @param thing The value to test for 'promiseness'
- * @param onDone The action to take if thing is resolved
- * @param onError The action to take if thing is rejected
- */
-Requisition.prototype._then = function(thing, onDone, onError) {
-  var then = null;
-  if (thing != null && typeof thing.then === 'function') {
-    // Simple promises with a then function
-    then = thing.then;
-  }
-  else if (thing != null && thing.promise != null &&
-                typeof thing.promise.then === 'function') {
-    // Deprecated: When we're passed a deferred rather than a promise
-    then = thing.promise.then;
-  }
-
-  if (then != null) {
-    then(onDone, onError);
-  }
-  else {
-    onDone(thing);
-  }
-};
+ * Helper to find the 'data-command' attribute, used by |update()|
+ */
+function getDataCommandAttribute(element) {
+  var command = element.getAttribute('data-command');
+  if (!command) {
+    command = element.querySelector('*[data-command]')
+                     .getAttribute('data-command');
+  }
+  return command;
+}
 
 /**
  * Called by the UI when ever the user interacts with a command line input
  * @param typed The contents of the input field
  */
 Requisition.prototype.update = function(typed) {
+  if (typeof HTMLElement !== 'undefined') {
+    if (typed instanceof HTMLElement) {
+      typed = getDataCommandAttribute(typed);
+    }
+    if (typed != null && typed.targetElement instanceof HTMLElement) {
+      typed = getDataCommandAttribute(typed.targetElement);
+    }
+  }
+
   this._structuralChangeInProgress = true;
 
-  this._args = this._tokenize(typed);
+  this._args = exports.tokenize(typed);
   var args = this._args.slice(0); // i.e. clone
 
   return this._split(args).then(function() {
     return this._assign(args).then(function() {
       this._structuralChangeInProgress = false;
       this.onTextChange();
     }.bind(this));
   }.bind(this));
@@ -6784,17 +6967,17 @@ Object.defineProperty(Requisition.protot
     }.bind(this));
 
     return summary;
   },
   enumerable: true
 });
 
 /**
- * Requisition._tokenize() is a state machine. These are the states.
+ * tokenize() is a state machine. These are the states.
  */
 var In = {
   /**
    * The last character was ' '.
    * Typing a ' ' character will not change the mode
    * Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT.
    * Anything else goes into SIMPLE mode.
    */
@@ -6837,17 +7020,17 @@ var In = {
 
 /**
  * Split up the input taking into account ', " and {.
  * We don't consider \t or other classical whitespace characters to split
  * arguments apart. For one thing these characters are hard to type, but also
  * if the user has gone to the trouble of pasting a TAB character into the
  * input field (or whatever it takes), they probably mean it.
  */
-Requisition.prototype._tokenize = function(typed) {
+exports.tokenize = function(typed) {
   // For blank input, place a dummy empty argument into the list
   if (typed == null || typed.length === 0) {
     return [ new Argument('', '', '') ];
   }
 
   if (isSimple(typed)) {
     return [ new Argument(typed, '', '') ];
   }
@@ -7040,24 +7223,42 @@ Requisition.prototype._split = function(
     // Special case: if the user enters { console.log('foo'); } then we need to
     // use the hidden 'eval' command
     conversion = new Conversion(evalCommand, new ScriptArgument());
     return this.setAssignment(this.commandAssignment, conversion, noArgUp);
   }
 
   var argsUsed = 1;
 
+  var promise;
   var commandType = this.commandAssignment.param.type;
   while (argsUsed <= args.length) {
     var arg = (argsUsed === 1) ?
               args[0] :
               new MergedArgument(args, 0, argsUsed);
-    // Making this promise synchronous is OK because we know that commandType
-    // is a synchronous type.
-    conversion = util.synchronize(commandType.parse(arg, this.context));
+
+    // Making the commandType.parse() promise as synchronous is OK because we
+    // know that commandType is a synchronous type.
+
+    if (this.prefix != null && this.prefix != '') {
+      var prefixArg = new Argument(this.prefix, '', ' ');
+      var prefixedArg = new MergedArgument([ prefixArg, arg ]);
+
+      promise = commandType.parse(prefixedArg, this.executionContext);
+      conversion = util.synchronize(promise);
+
+      if (conversion.value == null) {
+        promise = commandType.parse(arg, this.executionContext);
+        conversion = util.synchronize(promise);
+      }
+    }
+    else {
+      promise = commandType.parse(arg, this.executionContext);
+      conversion = util.synchronize(promise);
+    }
 
     // We only want to carry on if this command is a parent command,
     // which means that there is a commandAssignment, but not one with
     // an exec function.
     if (!conversion.value || conversion.value.exec) {
       break;
     }
 
@@ -7113,17 +7314,17 @@ Requisition.prototype._assign = function
     this._addUnassignedArgs(args);
     return util.all(outstanding);
   }
 
   // Special case: if there is only 1 parameter, and that's of type
   // text, then we put all the params into the first param
   if (this.assignmentCount === 1) {
     var assignment = this.getAssignment(0);
-    if (assignment.param.type instanceof StringType) {
+    if (assignment.param.type.name === 'string') {
       var arg = (args.length === 1) ? args[0] : new MergedArgument(args);
       outstanding.push(this.setAssignment(assignment, arg, noArgUp));
       return util.all(outstanding);
     }
   }
 
   // Positional arguments can still be specified by name, but if they are
   // then we need to ignore them when working them out positionally
@@ -7140,28 +7341,28 @@ Requisition.prototype._assign = function
     while (i < args.length) {
       if (assignment.param.isKnownAs(args[i].text)) {
         var arg = args.splice(i, 1)[0];
         unassignedParams = unassignedParams.filter(function(test) {
           return test !== assignment.param.name;
         });
 
         // boolean parameters don't have values, default to false
-        if (assignment.param.type instanceof BooleanType) {
+        if (assignment.param.type.name === 'boolean') {
           arg = new TrueNamedArgument(arg);
         }
         else {
           var valueArg = null;
           if (i + 1 <= args.length) {
             valueArg = args.splice(i, 1)[0];
           }
           arg = new NamedArgument(arg, valueArg);
         }
 
-        if (assignment.param.type instanceof ArrayType) {
+        if (assignment.param.type.name === 'array') {
           var arrayArg = arrayArgs[assignment.param.name];
           if (!arrayArg) {
             arrayArg = new ArrayArgument();
             arrayArgs[assignment.param.name] = arrayArg;
           }
           arrayArg.addArgument(arg);
         }
         else {
@@ -7183,34 +7384,34 @@ Requisition.prototype._assign = function
     // we have to default it to prevent previous values surviving
     if (!assignment.param.isPositionalAllowed) {
       outstanding.push(this.setAssignment(assignment, null, noArgUp));
       return;
     }
 
     // If this is a positional array argument, then it swallows the
     // rest of the arguments.
-    if (assignment.param.type instanceof ArrayType) {
+    if (assignment.param.type.name === 'array') {
       var arrayArg = arrayArgs[assignment.param.name];
       if (!arrayArg) {
         arrayArg = new ArrayArgument();
         arrayArgs[assignment.param.name] = arrayArg;
       }
       arrayArg.addArguments(args);
       args = [];
     }
     else {
       if (args.length === 0) {
         outstanding.push(this.setAssignment(assignment, null, noArgUp));
       }
       else {
         var arg = args.splice(0, 1)[0];
         // --foo and -f are named parameters, -4 is a number. So '-' is either
         // the start of a named parameter or a number depending on the context
-        var isIncompleteName = assignment.param.type instanceof NumberType ?
+        var isIncompleteName = assignment.param.type.name === 'number' ?
             /-[-a-zA-Z_]/.test(arg.text) :
             arg.text.charAt(0) === '-';
 
         if (isIncompleteName) {
           this._unassigned.push(new UnassignedAssignment(this, arg));
         }
         else {
           outstanding.push(this.setAssignment(assignment, arg, noArgUp));
@@ -7239,118 +7440,64 @@ exports.Requisition = Requisition;
 function Output(options) {
   options = options || {};
   this.command = options.command || '';
   this.args = options.args || {};
   this.typed = options.typed || '';
   this.canonical = options.canonical || '';
   this.hidden = options.hidden === true ? true : false;
 
-  this.type = this.command.returnType;
+  this.type = undefined;
   this.data = undefined;
   this.completed = false;
   this.error = false;
   this.start = new Date();
 
-  this.deferred = Promise.defer();
-  this.then = this.deferred.promise.then;
+  this._deferred = Promise.defer();
+  this.promise = this._deferred.promise;
 
   this.onClose = util.createEvent('Output.onClose');
-  this.onChange = util.createEvent('Output.onChange');
 }
 
 /**
- * Called when there is data to display, but the command is still executing
- * @param data The new data. If the data structure has been altered but the
- * root object is still the same, The same root object should be passed in the
- * data parameter.
- * @param ev Optional additional event data, for example to explain how the
- * data structure has changed
- */
-Output.prototype.changed = function(data, ev) {
+ * Called when there is data to display, and the command has finished executing
+ * See changed() for details on parameters.
+ */
+Output.prototype.complete = function(data, error) {
+  this.end = new Date();
+  this.duration = this.end.getTime() - this.start.getTime();
+  this.completed = true;
+  this.error = error;
+
   if (data != null && data.isTypedData) {
     this.data = data.data;
     this.type = data.type;
   }
   else {
     this.data = data;
+    this.type = this.command.returnType;
     if (this.type == null) {
-      this.type = typeof this.data;
-    }
-  }
-
-  ev = ev || {};
-  ev.output = this;
-  this.onChange(ev);
-};
-
-/**
- * Called when there is data to display, and the command has finished executing
- * See changed() for details on parameters.
- */
-Output.prototype.complete = function(data, error, ev) {
-  this.end = new Date();
-  this.duration = this.end.getTime() - this.start.getTime();
-  this.completed = true;
-  this.error = error;
-
-  this.changed(data, ev);
+      this.type = (this.data == null) ? 'undefined' : typeof this.data;
+    }
+  }
+
+  if (this.type === 'object') {
+    throw new Error('No type from output of ' + this.typed);
+  }
 
   if (error) {
-    this.deferred.reject();
+    this._deferred.reject();
   }
   else {
-    this.deferred.resolve();
+    this._deferred.resolve();
   }
 };
 
 exports.Output = Output;
 
-/**
- * Functions and data related to the execution of a command
- */
-exports.createExecutionContext = function(requisition) {
-  var context = {
-    exec: requisition.exec.bind(requisition),
-    update: requisition.update.bind(requisition),
-    updateExec: requisition.updateExec.bind(requisition),
-    createView: view.createView,
-    typedData: function(data, type) {
-      return {
-        isTypedData: true,
-        data: data,
-        type: type
-      };
-    },
-    getArgsObject: requisition.getArgsObject.bind(requisition),
-    defer: function() {
-      return Promise.defer();
-    },
-    /**
-     * @deprecated Use defer() instead, which does the same thing, but is not
-     * confusingly named
-     */
-    createPromise: function() {
-      return Promise.defer();
-    }
-  };
-
-  Object.defineProperty(context, 'environment', {
-    get: function() { return requisition.environment; },
-    enumerable : true
-  });
-
-  Object.defineProperty(context, 'document', {
-    get: function() { return requisition.document; },
-    enumerable : true
-  });
-
-  return context;
-};
-
 
 });
 define("text!gcli/ui/intro.html", [], "\n" +
   "<div>\n" +
   "  <p>${l10n.introTextOpening2}</p>\n" +
   "\n" +
   "  <p>\n" +
   "    ${l10n.introTextCommands}\n" +
@@ -7795,35 +7942,29 @@ exports.FocusManager = FocusManager;
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/fields/basic', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/types/basic', 'gcli/ui/fields'], function(require, exports, module) {
+define('gcli/ui/fields/basic', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/ui/fields'], function(require, exports, module) {
 
 'use strict';
 
 var util = require('util/util');
 var l10n = require('util/l10n');
 
 var Argument = require('gcli/argument').Argument;
 var TrueNamedArgument = require('gcli/argument').TrueNamedArgument;
 var FalseNamedArgument = require('gcli/argument').FalseNamedArgument;
 var ArrayArgument = require('gcli/argument').ArrayArgument;
 var ArrayConversion = require('gcli/types').ArrayConversion;
 
-var StringType = require('gcli/types/basic').StringType;
-var NumberType = require('gcli/types/basic').NumberType;
-var BooleanType = require('gcli/types/basic').BooleanType;
-var DelegateType = require('gcli/types/basic').DelegateType;
-var ArrayType = require('gcli/types/basic').ArrayType;
-
 var Field = require('gcli/ui/fields').Field;
 var fields = require('gcli/ui/fields');
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
@@ -7878,17 +8019,17 @@ StringField.prototype.setConversion = fu
 
 StringField.prototype.getConversion = function() {
   // This tweaks the prefix/suffix of the argument to fit
   this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true });
   return this.type.parse(this.arg, this.requisition.context);
 };
 
 StringField.claim = function(type, context) {
-  return type instanceof StringType ? Field.MATCH : Field.BASIC;
+  return type.name === 'string' ? Field.MATCH : Field.BASIC;
 };
 
 
 /**
  * A field that allows editing of numbers using an [input type=number] field
  */
 function NumberField(type, options) {
   Field.call(this, type, options);
@@ -7910,17 +8051,17 @@ function NumberField(type, options) {
   this.element.addEventListener('keyup', this.onInputChange, false);
 
   this.onFieldChange = util.createEvent('NumberField.onFieldChange');
 }
 
 NumberField.prototype = Object.create(Field.prototype);
 
 NumberField.claim = function(type, context) {
-  return type instanceof NumberType ? Field.MATCH : Field.NO_MATCH;
+  return type.name === 'number' ? Field.MATCH : Field.NO_MATCH;
 };
 
 NumberField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.element.removeEventListener('keyup', this.onInputChange, false);
   delete this.element;
   delete this.document;
   delete this.onInputChange;
@@ -7955,17 +8096,17 @@ function BooleanField(type, options) {
   this.element.addEventListener('change', this.onInputChange, false);
 
   this.onFieldChange = util.createEvent('BooleanField.onFieldChange');
 }
 
 BooleanField.prototype = Object.create(Field.prototype);
 
 BooleanField.claim = function(type, context) {
-  return type instanceof BooleanType ? Field.MATCH : Field.NO_MATCH;
+  return type.name === 'boolean' ? Field.MATCH : Field.NO_MATCH;
 };
 
 BooleanField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.element.removeEventListener('change', this.onInputChange, false);
   delete this.element;
   delete this.document;
   delete this.onInputChange;
@@ -8022,17 +8163,17 @@ DelegateField.prototype.update = functio
   this.field = fields.getField(subtype, this.options);
   this.field.onFieldChange.add(this.fieldChanged, this);
 
   util.clearElement(this.element);
   this.element.appendChild(this.field.element);
 };
 
 DelegateField.claim = function(type, context) {
-  return type instanceof DelegateType ? Field.MATCH : Field.NO_MATCH;
+  return type.isDelegate ? Field.MATCH : Field.NO_MATCH;
 };
 
 DelegateField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.requisition.onAssignmentChange.remove(this.update, this);
   delete this.element;
   delete this.document;
   delete this.onInputChange;
@@ -8084,17 +8225,17 @@ function ArrayField(type, options) {
   this.onInputChange = this.onInputChange.bind(this);
 
   this.onFieldChange = util.createEvent('ArrayField.onFieldChange');
 }
 
 ArrayField.prototype = Object.create(Field.prototype);
 
 ArrayField.claim = function(type, context) {
-  return type instanceof ArrayType ? Field.MATCH : Field.NO_MATCH;
+  return type.name === 'array' ? Field.MATCH : Field.NO_MATCH;
 };
 
 ArrayField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.addButton.removeEventListener('click', this._onAdd, false);
 };
 
 ArrayField.prototype.setConversion = function(conversion) {
@@ -8176,26 +8317,24 @@ ArrayField.prototype._onAdd = function(e
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/fields', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'gcli/types/basic'], function(require, exports, module) {
+define('gcli/ui/fields', ['require', 'exports', 'module' , 'util/promise', 'util/util'], function(require, exports, module) {
 
 'use strict';
 
 var Promise = require('util/promise');
 var util = require('util/util');
 var KeyEvent = require('util/util').KeyEvent;
 
-var BlankType = require('gcli/types/basic').BlankType;
-
 /**
  * A Field is a way to get input for a single parameter.
  * This class is designed to be inherited from. It's important that all
  * subclasses have a similar constructor signature because they are created
  * via getField(...)
  * @param type The type to use in conversions
  * @param options A set of properties to help fields configure themselves:
  * - document: The document we use in calling createElement
@@ -8396,17 +8535,17 @@ function BlankField(type, options) {
   this.element = util.createElement(this.document, 'div');
 
   this.onFieldChange = util.createEvent('BlankField.onFieldChange');
 }
 
 BlankField.prototype = Object.create(Field.prototype);
 
 BlankField.claim = function(type, context) {
-  return type instanceof BlankType ? Field.MATCH : Field.NO_MATCH;
+  return type.name === 'blank' ? Field.MATCH : Field.NO_MATCH;
 };
 
 BlankField.prototype.setConversion = function(conversion) {
   this.setMessage(conversion.message);
 };
 
 BlankField.prototype.getConversion = function() {
   return this.type.parse(new Argument(), this.requisition.context);
@@ -8427,27 +8566,26 @@ exports.addField(BlankField);
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/fields/javascript', ['require', 'exports', 'module' , 'util/util', 'util/promise', 'gcli/types', 'gcli/argument', 'gcli/types/javascript', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
+define('gcli/ui/fields/javascript', ['require', 'exports', 'module' , 'util/util', 'util/promise', 'gcli/types', 'gcli/argument', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
 
 'use strict';
 
 var util = require('util/util');
 var Promise = require('util/promise');
 
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
 var ScriptArgument = require('gcli/argument').ScriptArgument;
-var JavascriptType = require('gcli/types/javascript').JavascriptType;
 
 var Menu = require('gcli/ui/fields/menu').Menu;
 var Field = require('gcli/ui/fields').Field;
 var fields = require('gcli/ui/fields');
 
 
 /**
  * Registration and de-registration.
@@ -8494,17 +8632,17 @@ function JavascriptField(type, options) 
 
   // i.e. Register this.onItemClick as the default action for a menu click
   this.menu.onItemClick.add(this.itemClicked, this);
 }
 
 JavascriptField.prototype = Object.create(Field.prototype);
 
 JavascriptField.claim = function(type, context) {
-  return type instanceof JavascriptType ? Field.TOOLTIP_MATCH : Field.NO_MATCH;
+  return type.name === 'javascript' ? Field.TOOLTIP_MATCH : Field.NO_MATCH;
 };
 
 JavascriptField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.input.removeEventListener('keyup', this.onInputChange, false);
   this.menu.onItemClick.remove(this.itemClicked, this);
   this.menu.destroy();
   delete this.element;
@@ -8514,17 +8652,17 @@ JavascriptField.prototype.destroy = func
   delete this.onInputChange;
 };
 
 JavascriptField.prototype.setConversion = function(conversion) {
   this.arg = conversion.arg;
   this.input.value = conversion.arg.text;
 
   var prefixLen = 0;
-  if (this.type instanceof JavascriptType) {
+  if (this.type.name === 'javascript') {
     var typed = conversion.arg.text;
     var lastDot = typed.lastIndexOf('.');
     if (lastDot !== -1) {
       prefixLen = lastDot;
     }
   }
 
   this.setMessage(conversion.message);
@@ -8825,29 +8963,27 @@ define("text!gcli/ui/fields/menu.html", 
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/fields/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/types/basic', 'gcli/types/selection', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
+define('gcli/ui/fields/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
 
 'use strict';
 
 var Promise = require('util/promise');
 var util = require('util/util');
 var l10n = require('util/l10n');
 
 var Argument = require('gcli/argument').Argument;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
-var BooleanType = require('gcli/types/basic').BooleanType;
-var SelectionType = require('gcli/types/selection').SelectionType;
 
 var Menu = require('gcli/ui/fields/menu').Menu;
 var Field = require('gcli/ui/fields').Field;
 var fields = require('gcli/ui/fields');
 
 
 /**
  * Registration and de-registration.
@@ -8893,20 +9029,20 @@ function SelectionField(type, options) {
   this.element.addEventListener('change', this.onInputChange, false);
 
   this.onFieldChange = util.createEvent('SelectionField.onFieldChange');
 }
 
 SelectionField.prototype = Object.create(Field.prototype);
 
 SelectionField.claim = function(type, context) {
-  if (type instanceof BooleanType) {
+  if (type.name === 'boolean') {
     return Field.BASIC;
   }
-  return type instanceof SelectionType ? Field.DEFAULT : Field.NO_MATCH;
+  return type.isSelection ? Field.DEFAULT : Field.NO_MATCH;
 };
 
 SelectionField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.element.removeEventListener('change', this.onInputChange, false);
   delete this.element;
   delete this.document;
   delete this.onInputChange;
@@ -8955,17 +9091,17 @@ function SelectionTooltipField(type, opt
 
   // i.e. Register this.onItemClick as the default action for a menu click
   this.menu.onItemClick.add(this.itemClicked, this);
 }
 
 SelectionTooltipField.prototype = Object.create(Field.prototype);
 
 SelectionTooltipField.claim = function(type, context) {
-  return type.getType(context) instanceof SelectionType ?
+  return type.getType(context).isSelection ?
       Field.TOOLTIP_MATCH :
       Field.NO_MATCH;
 };
 
 SelectionTooltipField.prototype.destroy = function() {
   Field.prototype.destroy.call(this);
   this.menu.onItemClick.remove(this.itemClicked, this);
   this.menu.destroy();
@@ -9049,30 +9185,376 @@ SelectionTooltipField.DEFAULT_VALUE = '_
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/commands/help', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/canon', 'gcli/ui/view', 'text!gcli/commands/help_man.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help.css'], function(require, exports, module) {
+define('gcli/commands/connect', ['require', 'exports', 'module' , 'util/l10n', 'gcli/types', 'gcli/canon', 'util/connect/connector'], function(require, exports, module) {
 
 'use strict';
 
-var util = require('util/util');
+var l10n = require('util/l10n');
+var types = require('gcli/types');
+var canon = require('gcli/canon');
+var connector = require('util/connect/connector');
+
+/**
+ * A lookup of the current connection
+ */
+var connections = {};
+
+/**
+ * 'connect' command
+ */
+var connect = {
+  name: 'connect',
+  description: l10n.lookup('connectDesc'),
+  manual: l10n.lookup('connectManual'),
+  params: [
+    {
+      name: 'prefix',
+      type: 'string',
+      description: l10n.lookup('connectPrefixDesc')
+    },
+    {
+      name: 'host',
+      type: 'string',
+      description: l10n.lookup('connectHostDesc'),
+      defaultValue: 'localhost',
+      option: true
+    },
+    {
+      name: 'port',
+      type: { name: 'number', max: 65536, min: 0 },
+      description: l10n.lookup('connectPortDesc'),
+      defaultValue: connector.defaultPort,
+      option: true
+    }
+  ],
+  returnType: 'string',
+
+  exec: function(args, context) {
+    if (connections[args.prefix] != null) {
+      throw new Error(l10n.lookupFormat('connectDupReply', [ args.prefix ]));
+    }
+
+    var cxp = connector.connect(args.prefix, args.host, args.port);
+    return cxp.then(function(connection) {
+      connections[args.prefix] = connection;
+
+      return connection.getCommandSpecs().then(function(commandSpecs) {
+        var remoter = this.createRemoter(args.prefix, connection);
+        canon.addProxyCommands(args.prefix, commandSpecs, remoter);
+
+        // commandSpecs doesn't include the parent command that we added
+        return l10n.lookupFormat('connectReply',
+                                 [ Object.keys(commandSpecs).length + 1 ]);
+      }.bind(this));
+    }.bind(this));
+  },
+
+  /**
+   * When we register a set of remote commands, we need to provide the canon
+   * with a proxy executor. This is that executor.
+   */
+  createRemoter: function(prefix, connection) {
+    return function(cmdArgs, context) {
+      var typed = context.typed;
+      if (typed.indexOf(prefix) !== 0) {
+        throw new Error("Missing prefix");
+      }
+      typed = typed.substring(prefix.length).replace(/^ */, "");
+
+      return connection.execute(typed, cmdArgs).then(function(reply) {
+        var typedData = context.typedData(reply.type, reply.data);
+        if (!reply.error) {
+          return typedData;
+        }
+        else {
+          throw typedData;
+        }
+      });
+    }.bind(this);
+  }
+};
+
+/**
+ * 'connection' type
+ */
+var connection = {
+  name: 'connection',
+  parent: 'selection',
+  lookup: function() {
+    return Object.keys(connections).map(function(prefix) {
+      return { name: prefix, value: connections[prefix] };
+    });
+  }
+};
+
+/**
+ * 'disconnect' command
+ */
+var disconnect = {
+  name: 'disconnect',
+  description: l10n.lookup('disconnectDesc'),
+  manual: l10n.lookup('disconnectManual'),
+  params: [
+    {
+      name: 'prefix',
+      type: 'connection',
+      description: l10n.lookup('disconnectPrefixDesc'),
+    }
+  ],
+  returnType: 'string',
+
+  exec: function(args, context) {
+    return args.prefix.disconnect().then(function() {
+      var removed = canon.removeProxyCommands(args.prefix.prefix);
+      delete connections[args.prefix.prefix];
+      return l10n.lookupFormat('disconnectReply', [ removed.length ]);
+    });
+  }
+};
+
+
+/**
+ * Registration and de-registration.
+ */
+exports.startup = function() {
+  types.addType(connection);
+
+  canon.addCommand(connect);
+  canon.addCommand(disconnect);
+};
+
+exports.shutdown = function() {
+  canon.removeCommand(connect);
+  canon.removeCommand(disconnect);
+
+  types.removeType(connection);
+};
+
+
+});
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define('util/connect/connector', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+'use strict';
+
+/**
+ * Create a new Connection and begin the connect process so the connection
+ * object can't be used until it is connected.
+ */
+exports.connect = function(prefix, host, port) {
+  var builtinCommands = Components.utils.import('resource:///modules/devtools/BuiltinCommands.jsm', {});
+  return builtinCommands.connect(prefix, host, port);
+};
+
+/**
+ * What port should we use by default?
+ */
+Object.defineProperty(exports, 'defaultPort', {
+  get: function() {
+    Components.utils.import("resource://gre/modules/Services.jsm");
+    return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
+  },
+  enumerable: true
+});
+
+
+});
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define('gcli/commands/context', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon'], function(require, exports, module) {
+
+'use strict';
+
 var l10n = require('util/l10n');
 var canon = require('gcli/canon');
-var view = require('gcli/ui/view');
-
-// Storing the HTML on exports allows other builds to alter the help template
-// but still allowing dryice to do it's dependency thing properly
-exports.helpManHtml = require('text!gcli/commands/help_man.html');
-exports.helpListHtml = require('text!gcli/commands/help_list.html');
-exports.helpCss = require('text!gcli/commands/help.css');
+
+/**
+ * 'context' command
+ */
+var contextCmdSpec = {
+  name: 'context',
+  description: l10n.lookup('contextDesc'),
+  manual: l10n.lookup('contextManual'),
+  params: [
+   {
+     name: 'prefix',
+     type: 'command',
+     description: l10n.lookup('contextPrefixDesc'),
+     defaultValue: null
+   }
+  ],
+  returnType: 'string',
+  exec: function echo(args, context) {
+    // Do not copy this code
+    var requisition = context.__dlhjshfw;
+
+    if (args.prefix == null) {
+      requisition.prefix = null;
+      return l10n.lookup('contextEmptyReply');
+    }
+
+    if (args.prefix.exec != null) {
+      throw new Error(l10n.lookupFormat('contextNotParentError',
+                                        [ args.prefix.name ]));
+    }
+
+    requisition.prefix = args.prefix.name;
+    return l10n.lookupFormat('contextReply', [ args.prefix.name ]);
+  }
+};
+
+/**
+ * Registration and de-registration.
+ */
+exports.startup = function() {
+  canon.addCommand(contextCmdSpec);
+};
+
+exports.shutdown = function() {
+  canon.removeCommand(contextCmdSpec);
+};
+
+});
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define('gcli/commands/help', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon', 'gcli/converters', 'text!gcli/commands/help_man.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help.css'], function(require, exports, module) {
+
+'use strict';
+
+var l10n = require('util/l10n');
+var canon = require('gcli/canon');
+var converters = require('gcli/converters');
+
+var helpManHtml = require('text!gcli/commands/help_man.html');
+var helpListHtml = require('text!gcli/commands/help_list.html');
+var helpCss = require('text!gcli/commands/help.css');
+
+/**
+ * Convert a command into a man page
+ */
+var commandConverterSpec = {
+  from: 'commandData',
+  to: 'view',
+  exec: function(commandData, context) {
+    return context.createView({
+      html: helpManHtml,
+      options: { allowEval: true, stack: 'help_man.html' },
+      data: {
+        l10n: l10n.propertyLookup,
+        onclick: context.update,
+        ondblclick: context.updateExec,
+        describe: function(item) {
+          return item.manual || item.description;
+        },
+        getTypeDescription: function(param) {
+          var input = '';
+          if (param.defaultValue === undefined) {
+            input = l10n.lookup('helpManRequired');
+          }
+          else if (param.defaultValue === null) {
+            input = l10n.lookup('helpManOptional');
+          }
+          else {
+            input = param.defaultValue;
+          }
+          return '(' + param.type.name + ', ' + input + ')';
+        },
+        command: commandData.command,
+        subcommands: commandData.subcommands
+      },
+      css: helpCss,
+      cssId: 'gcli-help'
+    });
+  }
+};
+
+/**
+ * Convert a list of commands into a formatted list
+ */
+var commandsConverterSpec = {
+  from: 'commandsData',
+  to: 'view',
+  exec: function(commandsData, context) {
+    var heading;
+    if (commandsData.commands.length === 0) {
+      heading = l10n.lookupFormat('helpListNone', [ commandsData.prefix ]);
+    }
+    else if (commandsData.prefix == null) {
+      heading = l10n.lookup('helpListAll');
+    }
+    else {
+      heading = l10n.lookupFormat('helpListPrefix', [ commandsData.prefix ]);
+    }
+
+    return context.createView({
+      html: helpListHtml,
+      options: { allowEval: true, stack: 'help_list.html' },
+      data: {
+        l10n: l10n.propertyLookup,
+        includeIntro: commandsData.prefix == null,
+        heading: heading,
+        onclick: context.update,
+        ondblclick: context.updateExec,
+        matchingCommands: commandsData.commands
+      },
+      css: helpCss,
+      cssId: 'gcli-help'
+    });
+  }
+};
 
 /**
  * 'help' command
  */
 var helpCommandSpec = {
   name: 'help',
   description: l10n.lookup('helpDesc'),
   manual: l10n.lookup('helpManual'),
@@ -9080,153 +9562,393 @@ var helpCommandSpec = {
     {
       name: 'search',
       type: 'string',
       description: l10n.lookup('helpSearchDesc'),
       manual: l10n.lookup('helpSearchManual3'),
       defaultValue: null
     }
   ],
-  returnType: 'view',
 
   exec: function(args, context) {
-    var match = canon.getCommand(args.search || undefined);
-    if (match) {
-      return view.createView({
-        html: exports.helpManHtml,
-        options: { allowEval: true, stack: 'help_man.html' },
-        data: getManTemplateData(match, context),
-        css: exports.helpCss,
-        cssId: 'gcli-help'
+    var command = canon.getCommand(args.search || undefined);
+    if (command) {
+      return context.typedData('commandData', {
+        command: command,
+        subcommands: getSubCommands(command)
       });
     }
 
-    return view.createView({
-      html: exports.helpListHtml,
-      options: { allowEval: true, stack: 'help_list.html' },
-      data: getListTemplateData(args, context),
-      css: exports.helpCss,
-      cssId: 'gcli-help'
+    return context.typedData('commandsData', {
+      prefix: args.search,
+      commands: getMatchingCommands(args.search)
     });
   }
 };
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
   canon.addCommand(helpCommandSpec);
+  converters.addConverter(commandConverterSpec);
+  converters.addConverter(commandsConverterSpec);
 };
 
 exports.shutdown = function() {
   canon.removeCommand(helpCommandSpec);
+  converters.removeConverter(commandConverterSpec);
+  converters.removeConverter(commandsConverterSpec);
 };
 
 /**
  * Create a block of data suitable to be passed to the help_list.html template
  */
-function getListTemplateData(args, context) {
-  var matchingCommands = canon.getCommands().filter(function(command) {
+function getMatchingCommands(prefix) {
+  var commands = canon.getCommands().filter(function(command) {
     if (command.hidden) {
       return false;
     }
 
-    if (args.search && command.name.indexOf(args.search) !== 0) {
+    if (prefix && command.name.indexOf(prefix) !== 0) {
       // Filtered out because they don't match the search
       return false;
     }
-    if (!args.search && command.name.indexOf(' ') != -1) {
+    if (!prefix && command.name.indexOf(' ') != -1) {
       // We don't show sub commands with plain 'help'
       return false;
     }
     return true;
   });
-  matchingCommands.sort(function(c1, c2) {
+  commands.sort(function(c1, c2) {
+    return c1.name.localeCompare(c2.name);
+  });
+
+  return commands;
+}
+
+/**
+ * Find all the sub commands of the given command
+ */
+function getSubCommands(command) {
+  if (command.exec != null) {
+    return [];
+  }
+
+  var subcommands = canon.getCommands().filter(function(subcommand) {
+    return subcommand.name.indexOf(command.name) === 0 &&
+            subcommand.name !== command.name;
+  });
+
+  subcommands.sort(function(c1, c2) {
     return c1.name.localeCompare(c2.name);
   });
 
-  var heading;
-  if (matchingCommands.length === 0) {
-    heading = l10n.lookupFormat('helpListNone', [ args.search ]);
-  }
-  else if (args.search == null) {
-    heading = l10n.lookup('helpListAll');
-  }
-  else {
-    heading = l10n.lookupFormat('helpListPrefix', [ args.search ]);
-  }
-
+  return subcommands;
+}
+
+});
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define('gcli/converters', ['require', 'exports', 'module' , 'util/util', 'util/promise'], function(require, exports, module) {
+
+'use strict';
+
+var util = require('util/util');
+var Promise = require('util/promise');
+
+// It's probably easiest to read this bottom to top
+
+/**
+ * Best guess at creating a DOM element from random data
+ */
+var fallbackDomConverter = {
+  from: '*',
+  to: 'dom',
+  exec: function(data, conversionContext) {
+    if (data == null) {
+      return conversionContext.document.createTextNode('');
+    }
+
+    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
+      return data;
+    }
+
+    var node = util.createElement(conversionContext.document, 'p');
+    util.setContents(node, data.toString());
+    return node;
+  }
+};
+
+/**
+ * Best guess at creating a string from random data
+ */
+var fallbackStringConverter = {
+  from: '*',
+  to: 'string',
+  exec: function(data, conversionContext) {
+    if (data.isView) {
+      return data.toDom(conversionContext.document).textContent;
+    }
+
+    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
+      return data.textContent;
+    }
+
+    return data == null ? '' : data.toString();
+  }
+};
+
+/**
+ * Convert a view object to a DOM element
+ */
+var viewDomConverter = {
+  from: 'view',
+  to: 'dom',
+  exec: function(view, conversionContext) {
+    return view.toDom(conversionContext.document);
+  }
+};
+
+/**
+ * Convert a view object to a string
+ */
+var viewStringConverter = {
+  from: 'view',
+  to: 'string',
+  exec: function(view, conversionContext) {
+    return view.toDom(conversionContext.document).textContent;
+  }
+};
+
+/**
+ * Convert a terminal object (to help traditional CLI integration) to an element
+ */
+var terminalDomConverter = {
+  from: 'terminal',
+  to: 'dom',
+  createTextArea: function(text, conversionContext) {
+    var node = util.createElement(conversionContext.document, 'textarea');
+    node.classList.add('gcli-row-subterminal');
+    node.readOnly = true;
+    node.textContent = text;
+    return node;
+  },
+  exec: function(data, context) {
+    if (Array.isArray(data)) {
+      var node = util.createElement(conversionContext.document, 'div');
+      data.forEach(function(member) {
+        node.appendChild(this.createTextArea(member, conversionContext));
+      });
+      return node;
+    }
+    return this.createTextArea(data);
+  }
+};
+
+/**
+ * Several converters are just data.toString inside a 'p' element
+ */
+function nodeFromDataToString(data, conversionContext) {
+  var node = util.createElement(conversionContext.document, 'p');
+  node.textContent = data.toString();
+  return node;
+}
+
+/**
+ * Convert a string to a DOM element
+ */
+var stringDomConverter = {
+  from: 'string',
+  to: 'dom',
+  exec: nodeFromDataToString
+};
+
+/**
+ * Convert a number to a DOM element
+ */
+var numberDomConverter = {
+  from: 'number',
+  to: 'dom',
+  exec: nodeFromDataToString
+};
+
+/**
+ * Convert a number to a DOM element
+ */
+var booleanDomConverter = {
+  from: 'boolean',
+  to: 'dom',
+  exec: nodeFromDataToString
+};
+
+/**
+ * Convert a number to a DOM element
+ */
+var undefinedDomConverter = {
+  from: 'undefined',
+  to: 'dom',
+  exec: function(data, conversionContext) {
+    return util.createElement(conversionContext.document, 'span');
+  }
+};
+
+/**
+ * Convert a string to a DOM element
+ */
+var errorDomConverter = {
+  from: 'error',
+  to: 'dom',
+  exec: function(ex, conversionContext) {
+    var node = util.createElement(conversionContext.document, 'p');
+    node.className = "gcli-error";
+    node.textContent = ex;
+    return node;
+  }
+};
+
+/**
+ * Create a new converter by using 2 converters, one after the other
+ */
+function getChainConverter(first, second) {
+  if (first.to !== second.from) {
+    throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from);
+  }
   return {
-    l10n: l10n.propertyLookup,
-    includeIntro: args.search == null,
-    matchingCommands: matchingCommands,
-    heading: heading,
-
-    onclick: function(ev) {
-      util.updateCommand(ev.currentTarget, context);
-    },
-
-    ondblclick: function(ev) {
-      util.executeCommand(ev.currentTarget, context);
+    from: first.from,
+    to: second.to,
+    exec: function(data, conversionContext) {
+      var intermediate = first.exec(data, conversionContext);
+      return second.exec(intermediate, conversionContext);
     }
   };
 }
 
 /**
- * Create a block of data suitable to be passed to the help_man.html template
- */
-function getManTemplateData(command, context) {
-  var manTemplateData = {
-    l10n: l10n.propertyLookup,
-    command: command,
-
-    onclick: function(ev) {
-      util.updateCommand(ev.currentTarget, context);
-    },
-
-    ondblclick: function(ev) {
-      util.executeCommand(ev.currentTarget, context);
-    },
-
-    describe: function(item) {
-      return item.manual || item.description;
-    },
-
-    getTypeDescription: function(param) {
-      var input = '';
-      if (param.defaultValue === undefined) {
-        input = l10n.lookup('helpManRequired');
-      }
-      else if (param.defaultValue === null) {
-        input = l10n.lookup('helpManOptional');
-      }
-      else {
-        input = param.defaultValue;
-      }
-      return '(' + param.type.name + ', ' + input + ')';
-    }
-  };
-
-  Object.defineProperty(manTemplateData, 'subcommands', {
-    get: function() {
-      var matching = canon.getCommands().filter(function(subcommand) {
-        return subcommand.name.indexOf(command.name) === 0 &&
-                subcommand.name !== command.name;
-      });
-      matching.sort(function(c1, c2) {
-        return c1.name.localeCompare(c2.name);
-      });
-      return matching;
-    },
-    enumerable: true
-  });
-
-  return manTemplateData;
+ * This is where we cache the converters that we know about
+ */
+var converters = {
+  from: {}
+};
+
+/**
+ * Add a new converter to the cache
+ */
+exports.addConverter = function(converter) {
+  var fromMatch = converters.from[converter.from];
+  if (fromMatch == null) {
+    fromMatch = {};
+    converters.from[converter.from] = fromMatch;
+  }
+
+  fromMatch[converter.to] = converter;
+};
+
+/**
+ * Remove an existing converter from the cache
+ */
+exports.removeConverter = function(converter) {
+  var fromMatch = converters.from[converter.from];
+  if (fromMatch == null) {
+    return;
+  }
+
+  if (fromMatch[converter.to] === converter) {
+    fromMatch[converter.to] = null;
+  }
+};
+
+/**
+ * Work out the best converter that we've got, for a given conversion.
+ */
+function getConverter(from, to) {
+  var fromMatch = converters.from[from];
+  if (fromMatch == null) {
+    return getFallbackConverter(from, to);
+  }
+
+  var converter = fromMatch[to];
+  if (converter == null) {
+    // Someone is going to love writing a graph search algorithm to work out
+    // the smallest number of conversions, or perhaps the least 'lossy'
+    // conversion but for now the only 2 step conversion is foo->view->dom,
+    // which we are going to special case.
+    if (to === 'dom') {
+      converter = fromMatch['view'];
+      if (converter != null) {
+        return getChainConverter(converter, viewDomConverter);
+      }
+    }
+    if (to === 'string') {
+      converter = fromMatch['view'];
+      if (converter != null) {
+        return getChainConverter(converter, viewStringConverter);
+      }
+    }
+    return getFallbackConverter(from, to);
+  }
+  return converter;
 }
 
+/**
+ * Helper for getConverter to pick the best fallback converter
+ */
+function getFallbackConverter(from, to) {
+  console.error('No converter from ' + from + ' to ' + to + '. Using fallback');
+
+  if (to === 'dom') {
+    return fallbackDomConverter;
+  }
+
+  if (to === 'string') {
+    return fallbackStringConverter;
+  }
+
+  throw new Error('No conversion possible from ' + from + ' to ' + to + '.');
+}
+
+/**
+ * Convert some data from one type to another
+ * @param data The object to convert
+ * @param from The type of the data right now
+ * @param to The type that we would like the data in
+ * @param conversionContext An execution context (i.e. simplified requisition) which is
+ * often required for access to a document, or createView function
+ */
+exports.convert = function(data, from, to, conversionContext) {
+  if (from === to) {
+    return Promise.resolve(data);
+  }
+  return Promise.resolve(getConverter(from, to).exec(data, conversionContext));
+};
+
+exports.addConverter(viewDomConverter);
+exports.addConverter(viewStringConverter);
+exports.addConverter(terminalDomConverter);
+exports.addConverter(stringDomConverter);
+exports.addConverter(numberDomConverter);
+exports.addConverter(booleanDomConverter);
+exports.addConverter(undefinedDomConverter);
+exports.addConverter(errorDomConverter);
+
+
 });
 define("text!gcli/commands/help_man.html", [], "\n" +
   "<div>\n" +
   "  <h3>${command.name}</h3>\n" +
   "\n" +
   "  <h4 class=\"gcli-help-header\">\n" +
   "    ${l10n.helpManSynopsis}:\n" +
   "    <span class=\"gcli-out-shortcut\" onclick=\"${onclick}\" data-command=\"${command.name}\">\n" +
@@ -9378,34 +10100,34 @@ var prefSetCmdSpec = {
       type: 'settingValue',
       description: l10n.lookup('prefSetValueDesc'),
       manual: l10n.lookup('prefSetValueManual')
     }
   ],
   exec: function(args, context) {
     if (!exports.allowSet.value &&
         args.setting.name !== exports.allowSet.name) {
-      return context.typedData(null, 'prefSetWarning');
+      return context.typedData('prefSetWarning', null);
     }
 
     args.setting.value = args.value;
   }
 };
 
 var prefSetWarningConverterSpec = {
   from: 'prefSetWarning',
   to: 'view',
   exec: function(data, context) {
     return context.createView({
       html: require('text!gcli/commands/pref_set_check.html'),
       options: { allowEval: true, stack: 'pref_set_check.html' },
       data: {
         l10n: l10n.propertyLookup,
         activate: function() {
-          context.exec('pref set ' + exports.allowSet.name + ' true');
+          context.updateExec('pref set ' + exports.allowSet.name + ' true');
         }
       }
     });
   }
 };
 
 /**
  * 'pref reset' command
@@ -9448,255 +10170,16 @@ exports.shutdown = function() {
   converters.removeConverter(prefSetWarningConverterSpec);
 
   settings.removeSetting(allowSetSettingSpec);
   exports.allowSet = undefined;
 };
 
 
 });
-/*
- * Copyright 2012, Mozilla Foundation and contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-define('gcli/converters', ['require', 'exports', 'module' , 'util/util', 'util/promise'], function(require, exports, module) {
-
-'use strict';
-
-var util = require('util/util');
-var Promise = require('util/promise');
-
-// It's probably easiest to read this bottom to top
-
-/**
- * Best guess at creating a DOM element from random data
- */
-var fallbackDomConverter = {
-  from: '*',
-  to: 'dom',
-  exec: function(data, context) {
-    if (data == null) {
-      return context.document.createTextNode('');
-    }
-
-    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
-      return data;
-    }
-
-    var node = util.createElement(context.document, 'p');
-    util.setContents(node, data.toString());
-    return node;
-  }
-};
-
-/**
- * Best guess at creating a string from random data
- */
-var fallbackStringConverter = {
-  from: '*',
-  to: 'string',
-  exec: function(data, context) {
-    if (data.isView) {
-      return data.toDom(context.document).textContent;
-    }
-
-    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
-      return data.textContent;
-    }
-
-    return data == null ? '' : data.toString();
-  }
-};
-
-/**
- * Convert a view object to a DOM element
- */
-var viewDomConverter = {
-  from: 'view',
-  to: 'dom',
-  exec: function(data, context) {
-    return data.toDom(context.document);
-  }
-};
-
-/**
- * Convert a terminal object (to help traditional CLI integration) to an element
- */
-var terminalDomConverter = {
-  from: 'terminal',
-  to: 'dom',
-  createTextArea: function(text) {
-    var node = util.createElement(context.document, 'textarea');
-    node.classList.add('gcli-row-subterminal');
-    node.readOnly = true;
-    node.textContent = text;
-    return node;
-  },
-  exec: function(data, context) {
-    if (Array.isArray(data)) {
-      var node = util.createElement(context.document, 'div');
-      data.forEach(function(member) {
-        node.appendChild(this.createTextArea(member));
-      });
-      return node;
-    }
-    return this.createTextArea(data);
-  }
-};
-
-/**
- * Convert a string to a DOM element
- */
-var stringDomConverter = {
-  from: 'string',
-  to: 'dom',
-  exec: function(data, context) {
-    var node = util.createElement(context.document, 'p');
-    node.textContent = data;
-    return node;
-  }
-};
-
-/**
- * Convert a string to a DOM element
- */
-var exceptionDomConverter = {
-  from: 'exception',
-  to: 'dom',
-  exec: function(ex, context) {
-    var node = util.createElement(context.document, 'p');
-    node.className = "gcli-exception";
-    node.textContent = ex;
-    return node;
-  }
-};
-
-/**
- * Create a new converter by using 2 converters, one after the other
- */
-function getChainConverter(first, second) {
-  if (first.to !== second.from) {
-    throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from);
-  }
-  return {
-    from: first.from,
-    to: second.to,
-    exec: function(data, context) {
-      var intermediate = first.exec(data, context);
-      return second.exec(intermediate, context);
-    }
-  };
-}
-
-/**
- * This is where we cache the converters that we know about
- */
-var converters = {
-  from: {}
-};
-
-/**
- * Add a new converter to the cache
- */
-exports.addConverter = function(converter) {
-  var fromMatch = converters.from[converter.from];
-  if (fromMatch == null) {
-    fromMatch = {};
-    converters.from[converter.from] = fromMatch;
-  }
-
-  fromMatch[converter.to] = converter;
-};
-
-/**
- * Remove an existing converter from the cache
- */
-exports.removeConverter = function(converter) {
-  var fromMatch = converters.from[converter.from];
-  if (fromMatch == null) {
-    return;
-  }
-
-  if (fromMatch[converter.to] === converter) {
-    fromMatch[converter.to] = null;
-  }
-};
-
-/**
- * Work out the best converter that we've got, for a given conversion.
- */
-function getConverter(from, to) {
-  var fromMatch = converters.from[from];
-  if (fromMatch == null) {
-    return getFallbackConverter(to);
-  }
-  var converter = fromMatch[to];
-  if (converter == null) {
-    // Someone is going to love writing a graph search algorithm to work out
-    // the smallest number of conversions, or perhaps the least 'lossy'
-    // conversion but for now the only 2 step conversion is foo->view->dom,
-    // which we are going to special case.
-    if (to === 'dom') {
-      converter = fromMatch['view'];
-      if (converter != null) {
-        return getChainConverter(converter, viewDomConverter);
-      }
-    }
-    return getFallbackConverter(to);
-  }
-  return converter;
-}
-
-/**
- * Helper for getConverter to pick the best fallback converter
- */
-function getFallbackConverter(to) {
-  if (to == 'dom') {
-    return fallbackDomConverter;
-  }
-  if (to == 'string') {
-    return fallbackStringConverter;
-  }
-  throw new Error('No conversion possible from ' + from + ' to ' + to + '.');
-}
-
-/**
- * Convert some data from one type to another
- * @param data The object to convert
- * @param from The type of the data right now
- * @param to The type that we would like the data in
- * @param context An execution context (i.e. simplified requisition) which is
- * often required for access to a document, or createView function
- */
-exports.convert = function(data, from, to, context) {
-  if (from === to) {
-    return data;
-  }
-  return Promise.resolve(getConverter(from, to).exec(data, context));
-};
-
-exports.addConverter(viewDomConverter);
-exports.addConverter(terminalDomConverter);
-exports.addConverter(stringDomConverter);
-exports.addConverter(exceptionDomConverter);
-
-
-});
 define("text!gcli/commands/pref_set_check.html", [], "<div>\n" +
   "  <p><strong>${l10n.prefSetCheckHeading}</strong></p>\n" +
   "  <p>${l10n.prefSetCheckBody}</p>\n" +
   "  <button onclick=\"${activate}\">${l10n.prefSetCheckGo}</button>\n" +
   "</div>\n" +
   "");
 
 /*
--- a/browser/devtools/commandline/test/Makefile.in
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -26,16 +26,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_cmd_appcache_valid.js \
   browser_cmd_appcache_valid_appcache.appcache \
   browser_cmd_appcache_valid_appcache.appcache^headers^ \
   browser_cmd_appcache_valid_index.html \
   browser_cmd_appcache_valid_page1.html \
   browser_cmd_appcache_valid_page2.html \
   browser_cmd_appcache_valid_page3.html \
   browser_cmd_commands.js \
+  browser_cmd_cookie.html \
   browser_cmd_cookie.js \
   browser_cmd_jsb.js \
   browser_cmd_jsb_script.jsi \
   browser_cmd_pagemod_export.html \
   browser_cmd_pagemod_export.js \
   browser_cmd_pref.js \
   browser_cmd_restart.js \
   browser_cmd_screenshot.html \
--- a/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
@@ -51,18 +51,18 @@ function test() {
             status: 'VALID',
             args: {
               search: { value: 'page' },
             }
           },
           exec: {
             output: [ /page1/, /page2/, /page3/ ]
           },
-          post: function(output) {
-            ok(!output.contains("index"), "index is not contained in output");
+          post: function(output, text) {
+            ok(!text.contains("index"), "index is not contained in output");
           }
         },
 
         {
           setup: 'appcache validate',
           check: {
             input:  'appcache validate',
             markup: 'VVVVVVVVVVVVVVVVV',
@@ -113,21 +113,21 @@ function test() {
             input:  'appcache list',
             markup: 'VVVVVVVVVVVVV',
             status: 'VALID',
             args: {},
           },
           exec: {
             output: [ /no results/ ]
           },
-          post: function(output) {
-            ok(!output.contains("index"), "index is not contained in output");
-            ok(!output.contains("page1"), "page1 is not contained in output");
-            ok(!output.contains("page2"), "page1 is not contained in output");
-            ok(!output.contains("page3"), "page1 is not contained in output");
+          post: function(output, text) {
+            ok(!text.contains("index"), "index is not contained in output");
+            ok(!text.contains("page1"), "page1 is not contained in output");
+            ok(!text.contains("page2"), "page1 is not contained in output");
+            ok(!text.contains("page3"), "page1 is not contained in output");
           }
         },
 
         {
           setup: 'appcache viewentry --key ' + TEST_URI,
           check: {
             input:  'appcache viewentry --key ' + TEST_URI,
                   // appcache viewentry --key http://sub1.test2.example.com/browser/browser/devtools/commandline/test/browser_cmd_appcache_valid_index.html
--- a/browser/devtools/commandline/test/browser_cmd_commands.js
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -10,27 +10,16 @@ const TEST_URI = "data:text/html;charset
 let tests = {};
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.runTests(options, tests);
   }).then(finish);
 }
 
-tests.testEcho = function(options) {
-  return helpers.audit(options, [
-    {
-      setup: "echo message",
-      exec: {
-        output: "message",
-      }
-    }
-  ]);
-};
-
 tests.testConsole = function(options) {
   let deferred = Promise.defer();
   let hud = null;
 
   let onWebConsoleOpen = function(subject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     subject.QueryInterface(Ci.nsISupportsString);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>GCLI cookie command test</title>
+</head>
+<body>
+
+  <p>Cookie test</p>
+  <p id=result></p>
+  <script type="text/javascript">
+    document.cookie = "zap=zep";
+    document.cookie = "zip=zop";
+    document.getElementById("result").innerHTML = document.cookie;
+  </script>
+
+</body>
+</html>
--- a/browser/devtools/commandline/test/browser_cmd_cookie.js
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the cookie commands works as they should
 
-const TEST_URI = "data:text/html;charset=utf-8,gcli-cookie";
+const TEST_URI = "http://example.com/browser/browser/devtools/commandline/"+
+                 "test/browser_cmd_cookie.html";
 
 function test() {
   helpers.addTabWithToolbar(TEST_URI, function(options) {
     return helpers.audit(options, [
       {
         setup: 'cookie',
         check: {
           input:  'cookie',
@@ -74,49 +75,81 @@ function test() {
             value: { value: 'ban' },
             secure: { value: false },
           }
         },
       },
       {
         setup: "cookie list",
         exec: {
-          output: 'No cookies found for host'
+          output: [ /zap=zep/, /zip=zop/, /Edit/ ]
         }
       },
       {
-        setup: "cookie set fruit banana",
+        setup: "cookie set zup banana",
         check: {
           args: {
-            name: { value: 'fruit' },
+            name: { value: 'zup' },
             value: { value: 'banana' },
           }
         },
         exec: {
           output: ""
         }
       },
       {
         setup: "cookie list",
         exec: {
-          output: [ /fruit=banana/, /Expires:/, /Edit/ ]
+          output: [ /zap=zep/, /zip=zop/, /zup=banana/, /Edit/ ]
         }
       },
       {
-        setup: "cookie remove fruit",
-        check: {
-          args: {
-            name: { value: 'fruit' },
-          }
+        setup: "cookie remove zip",
+        exec: { },
+      },
+      {
+        setup: "cookie list",
+        exec: {
+          output: [ /zap=zep/, /zup=banana/, /Edit/ ]
         },
-        exec: {
-          output: ""
+        post: function(output, text) {
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
         }
       },
       {
+        setup: "cookie remove zap",
+        exec: { },
+      },
+      {
         setup: "cookie list",
         exec: {
-          output: 'No cookies found for host'
+          output: [ /zup=banana/, /Edit/ ]
+        },
+        post: function(output, text) {
+          ok(!text.contains("zap"), "");
+          ok(!text.contains("zep"), "");
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
+        }
+      },
+      {
+        setup: "cookie remove zup",
+        exec: { }
+      },
+      {
+        setup: "cookie list",
+        exec: {
+          output: 'No cookies found for host example.com'
+        },
+        post: function(output, text) {
+          ok(!text.contains("zap"), "");
+          ok(!text.contains("zep"), "");
+          ok(!text.contains("zip"), "");
+          ok(!text.contains("zop"), "");
+          ok(!text.contains("zup"), "");
+          ok(!text.contains("banana"), "");
+          ok(!text.contains("Edit"), "");
         }
       },
     ]);
   }).then(finish);
 }
--- a/browser/devtools/commandline/test/browser_gcli_canon.js
+++ b/browser/devtools/commandline/test/browser_gcli_canon.js
@@ -33,16 +33,17 @@ function test() {
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var helpers = require('gclitest/helpers');
 var canon = require('gcli/canon');
 // var assert = require('test/assert');
+var Canon = canon.Canon;
 
 var startCount = undefined;
 var events = undefined;
 
 var canonChange = function(ev) {
   events++;
 };
 
@@ -152,18 +153,16 @@ exports.testAddRemove2 = function(option
   assert.is(canon.getCommands().length,
             startCount + 1,
             'rereadd command success');
   assert.is(events, 4, 'rereadd event');
 
   return helpers.audit(options, [
     {
       setup: 'testadd',
-      check: {
-      },
       exec: {
         output: /^3$/
       },
       post: function() {
         canon.removeCommand({
           name: 'testadd'
         });
 
@@ -194,9 +193,79 @@ exports.testAddRemove3 = function(option
   assert.is(canon.getCommands().length,
             startCount,
             'nonexistant2 command success');
   assert.is(events, 5, 'nonexistant2 event');
 
   canon.onCanonChange.remove(canonChange);
 };
 
+exports.testAltCanon = function(options) {
+  var altCanon = new Canon();
+
+  var tss = {
+    name: 'tss',
+    params: [
+      { name: 'str', type: 'string' },
+      { name: 'num', type: 'number' },
+      { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } },
+    ],
+    exec: function(args, context) {
+      return context.commandName + ':' +
+              args.str + ':' + args.num + ':' + args.opt;
+    }
+  };
+  altCanon.addCommand(tss);
+
+  var commandSpecs = altCanon.getCommandSpecs();
+  assert.is(JSON.stringify(commandSpecs),
+            '{"tss":{"name":"tss","params":[' +
+              '{"name":"str","type":"string"},' +
+              '{"name":"num","type":"number"},' +
+              '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}]}}',
+            'JSON.stringify(commandSpecs)');
+
+  var remoter = function(args, context) {
+    assert.is(context.commandName, 'tss', 'commandName is tss');
+
+    var cmd = altCanon.getCommand(context.commandName);
+    return cmd.exec(args, context);
+  };
+
+  canon.addProxyCommands('proxy', commandSpecs, remoter, 'test');
+
+  var parent = canon.getCommand('proxy');
+  assert.is(parent.name, 'proxy', 'Parent command called proxy');
+
+  var child = canon.getCommand('proxy tss');
+  assert.is(child.name, 'proxy tss', 'child command called proxy tss');
+
+  return helpers.audit(options, [
+    {
+      setup:    'proxy tss foo 6 3',
+      check: {
+        input:  'proxy tss foo 6 3',
+        hints:                    '',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        cursor: 17,
+        status: 'VALID',
+        args: {
+          str: { value: 'foo', status: 'VALID' },
+          num: { value: 6, status: 'VALID' },
+          opt: { value: '3', status: 'VALID' }
+        }
+      },
+      exec: {
+        output: 'tss:foo:6:3'
+      },
+      post: function() {
+        canon.removeCommand('proxy');
+        canon.removeCommand('proxy tss');
+
+        assert.is(canon.getCommand('proxy'), undefined, 'remove proxy');
+        assert.is(canon.getCommand('proxy tss'), undefined, 'remove proxy tss');
+      }
+    }
+  ]);
+};
+
+
 // });
--- a/browser/devtools/commandline/test/browser_gcli_cli.js
+++ b/browser/devtools/commandline/test/browser_gcli_cli.js
@@ -753,17 +753,16 @@ exports.testSingleFloat = function(optio
       check: {
         input:  'tsf',
         hints:     ' <num>',
         markup: 'VVV',
         cursor: 3,
         current: '__command',
         status: 'ERROR',
         error: '',
-        predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: 'tsf' },
           num: {
             value: undefined,
             arg: '',
             status: 'INCOMPLETE',
             message: ''
--- a/browser/devtools/commandline/test/browser_gcli_completion.js
+++ b/browser/devtools/commandline/test/browser_gcli_completion.js
@@ -514,19 +514,19 @@ exports.testSpaceComplete = function(opt
         current: 'sel2',
         status: 'ERROR',
         tooltipState: 'true:importantFieldFlag',
         args: {
           command: { name: 'tslong' },
           msg: { status: 'INCOMPLETE', message: '' },
           num: { status: 'VALID' },
           sel: { status: 'VALID' },
-          bool: { value: false,status: 'VALID' },
+          bool: { value: false, status: 'VALID' },
           num2: { status: 'VALID' },
-          bool2: { value: false,status: 'VALID' },
+          bool2: { value: false, status: 'VALID' },
           sel2: {
             value: 'with space',
             arg: ' --sel2 \'with space\' ',
             status: 'VALID'
           }
         }
       }
     }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_context.js
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2012, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testContext.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+  mockCommands.setup();
+
+  origLogErrors = cli.logErrors;
+  cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+
+  cli.logErrors = origLogErrors;
+  origLogErrors = undefined;
+};
+
+exports.testBaseline = function(options) {
+  helpers.audit(options, [
+    // These 3 establish a baseline for comparison when we have used the
+    // context command
+    {
+      setup:    'ext',
+      check: {
+        input:  'ext',
+        hints:     ' -> context',
+        markup: 'III',
+        message: '',
+        predictions: [ 'context', 'tsn ext', 'tsn exte', 'tsn exten', 'tsn extend' ],
+        unassigned: [ ],
+      }
+    },
+    {
+      setup:    'ext test',
+      check: {
+        input:  'ext test',
+        hints:          '',
+        markup: 'IIIVEEEE',
+        status: 'ERROR',
+        message: 'Too many arguments',
+        unassigned: [ ' test' ],
+      }
+    },
+    {
+      setup:    'tsn',
+      check: {
+        input:  'tsn',
+        hints:     '',
+        markup: 'III',
+        cursor: 3,
+        current: '__command',
+        status: 'ERROR',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'tsn' },
+        }
+      }
+    }
+  ]);
+};
+
+exports.testContext = function(options) {
+  helpers.audit(options, [
+    // Use the 'tsn' context
+    {
+      setup:    'context tsn',
+      check: {
+        input:  'context tsn',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        message: '',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsn,
+            status: 'VALID',
+            message: ''
+          },
+        }
+      },
+      exec: {
+        output: 'Using tsn as a command prefix',
+        completed: true,
+      }
+    },
+    // For comparison with earlier
+    {
+      setup:    'ext',
+      check: {
+        input:  'ext',
+        hints:     ' <text>',
+        markup: 'VVV',
+        predictions: [ 'tsn ext', 'tsn exte', 'tsn exten', 'tsn extend' ],
+        args: {
+          command: { name: 'tsn ext' },
+          text: {
+            value: undefined,
+            arg: '',
+            status: 'INCOMPLETE',
+            message: ''
+          },
+        }
+      }
+    },
+    {
+      setup:    'ext test',
+      check: {
+        input:  'ext test',
+        hints:          '',
+        markup: 'VVVVVVVV',
+        args: {
+          command: { name: 'tsn ext' },
+          text: {
+            value: 'test',
+            arg: ' test',
+            status: 'VALID',
+            message: ''
+          },
+        }
+      },
+      exec: {
+        output: 'Exec: tsnExt text=test',
+        completed: true,
+      }
+    },
+    {
+      setup:    'tsn',
+      check: {
+        input:  'tsn',
+        hints:     '',
+        markup: 'III',
+        message: '',
+        predictionsContains: [ 'tsn', 'tsn deep', 'tsn ext', 'tsn exte' ],
+        args: {
+          command: { name: 'tsn' },
+        }
+      }
+    },
+    // Does it actually work?
+    {
+      setup:    'tsb true',
+      check: {
+        input:  'tsb true',
+        hints:          '',
+        markup: 'VVVVVVVV',
+        options: [ 'true' ],
+        message: '',
+        predictions: [ 'true' ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'tsb' },
+          toggle: { value: true, arg: ' true', status: 'VALID', message: '' },
+        }
+      }
+    },
+    {
+      // Bug 866710 - GCLI should allow argument merging for non-string parameters
+      setup: 'context tsn ext',
+      skip: true
+    },
+    {
+      setup:    'context "tsn ext"',
+      check: {
+        input:  'context "tsn ext"',
+        hints:                   '',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        message: '',
+        predictions: [ ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsnExt,
+            status: 'VALID',
+            message: ''
+          }
+        }
+      },
+      exec: {
+        output: 'Error: Can\'t use \'tsn ext\' as a prefix because it is not a parent command.',
+        completed: true,
+        error: true
+      }
+    },
+    /*
+    {
+      setup:    'context "tsn deep"',
+      check: {
+        input:  'context "tsn deep"',
+        hints:                    '',
+        markup: 'VVVVVVVVVVVVVVVVVV',
+        status: 'ERROR',
+        message: '',
+        predictions: [ 'tsn deep' ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: {
+            value: mockCommands.commands.tsnDeep,
+            status: 'VALID',
+            message: ''
+          }
+        }
+      },
+      exec: {
+        output: '',
+        completed: true,
+      }
+    },
+    */
+    {
+      setup:    'context',
+      check: {
+        input:  'context',
+        hints:         ' [prefix]',
+        markup: 'VVVVVVV',
+        status: 'VALID',
+        unassigned: [ ],
+        args: {
+          command: { name: 'context' },
+          prefix: { value: undefined, arg: '', status: 'VALID', message: '' },
+        }
+      },
+      exec: {
+        output: 'Command prefix is unset',
+        completed: true,
+        type: 'string',
+        error: false
+      }
+    }
+  ]);
+};
+
+
+// });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_fail.js
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// define(function(require, exports, module) {
+
+// <INJECTED SOURCE:START>
+
+// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
+// DO NOT EDIT IT DIRECTLY
+
+var exports = {};
+
+const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFail.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+var cli = require('gcli/cli');
+
+var origLogErrors = undefined;
+
+exports.setup = function(options) {
+  mockCommands.setup();
+
+  origLogErrors = cli.logErrors;
+  cli.logErrors = false;
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+
+  cli.logErrors = origLogErrors;
+  origLogErrors = undefined;
+};
+
+exports.testBasic = function(options) {
+  return helpers.audit(options, [
+    {
+      setup: 'tsfail reject',
+      exec: {
+        completed: false,
+        output: 'rejected promise',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail rejecttyped',
+      exec: {
+        completed: false,
+        output: '54',
+        type: 'number',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail throwerror',
+      exec: {
+        completed: true,
+        output: 'Error: thrown error',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail throwstring',
+      exec: {
+        completed: true,
+        output: 'thrown string',
+        type: 'error',
+        error: true
+      }
+    },
+    {
+      setup: 'tsfail noerror',
+      exec: {
+        completed: true,
+        output: 'no error',
+        type: 'string',
+        error: false
+      }
+    }
+  ]);
+};
+
+
+// });
--- a/browser/devtools/commandline/test/browser_gcli_inputter.js
+++ b/browser/devtools/commandline/test/browser_gcli_inputter.js
@@ -35,48 +35,40 @@ function test() {
 
 'use strict';
 
 var KeyEvent = require('util/util').KeyEvent;
 // var assert = require('test/assert');
 // var mockCommands = require('gclitest/mockCommands');
 
 var latestEvent = undefined;
-var latestOutput = undefined;
 var latestData = undefined;
 
 var outputted = function(ev) {
-  function updateData() {
-    latestData = latestOutput.data;
-  }
+  latestEvent = ev;
 
-  if (latestOutput != null) {
-    ev.output.onChange.remove(updateData);
-  }
-
-  latestEvent = ev;
-  latestOutput = ev.output;
-
-  ev.output.onChange.add(updateData);
+  ev.output.promise.then(function() {
+    latestData = ev.output.data;
+    ev.output.onClose();
+  });
 };
 
 
 exports.setup = function(options) {
   mockCommands.setup();
   options.display.requisition.commandOutputManager.onOutput.add(outputted);
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
   options.display.requisition.commandOutputManager.onOutput.remove(outputted);
 };
 
 exports.testOutput = function(options) {
   latestEvent = undefined;
-  latestOutput = undefined;
   latestData = undefined;
 
   var inputter = options.display.inputter;
   var focusManager = options.display.focusManager;
 
   inputter.setInput('tss');
 
   var ev0 = { keyCode: KeyEvent.DOM_VK_RETURN };
@@ -98,18 +90,16 @@ exports.testOutput = function(options) {
     var ev2 = { keyCode: KeyEvent.DOM_VK_F1 };
     return inputter.handleKeyUp(ev2).then(function() {
       assert.ok(!focusManager._recentOutput, 'no recent output happened post F1');
       assert.ok(focusManager._helpRequested, 'F1 = help');
 
       var ev3 = { keyCode: KeyEvent.DOM_VK_ESCAPE };
       return inputter.handleKeyUp(ev3).then(function() {
         assert.ok(!focusManager._helpRequested, 'ESCAPE = anti help');
-
-        latestOutput.onClose();
       });
     });
 
   });
 };
 
 
 // });
--- a/browser/devtools/commandline/test/browser_gcli_resource.js
+++ b/browser/devtools/commandline/test/browser_gcli_resource.js
@@ -56,87 +56,87 @@ exports.shutdown = function(options) {
 };
 
 exports.testAllPredictions1 = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType('resource');
+  var resource = types.createType('resource');
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length > 1, 'have all resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testScriptPredictions = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType({ name: 'resource', include: 'text/javascript' });
+  var resource = types.createType({ name: 'resource', include: 'text/javascript' });
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length > 1, 'have js resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testStylePredictions = function(options) {
   if (options.isFirefox || options.isJsdom) {
     assert.log('Skipping checks due to jsdom/firefox document.stylsheets support.');
     return;
   }
 
-  var resource = types.getType({ name: 'resource', include: 'text/css' });
+  var resource = types.createType({ name: 'resource', include: 'text/css' });
   return resource.getLookup().then(function(opts) {
     assert.ok(opts.length >= 1, 'have css resources');
 
     return util.promiseEach(opts, function(prediction) {
       return checkPrediction(resource, prediction);
     });
   });
 };
 
 exports.testAllPredictions2 = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping checks due to jsdom document.stylsheets support.');
     return;
   }
 
-  var scriptRes = types.getType({ name: 'resource', include: 'text/javascript' });
+  var scriptRes = types.createType({ name: 'resource', include: 'text/javascript' });
   return scriptRes.getLookup().then(function(scriptOptions) {
-    var styleRes = types.getType({ name: 'resource', include: 'text/css' });
+    var styleRes = types.createType({ name: 'resource', include: 'text/css' });
     return styleRes.getLookup().then(function(styleOptions) {
-      var allRes = types.getType({ name: 'resource' });
+      var allRes = types.createType({ name: 'resource' });
       return allRes.getLookup().then(function(allOptions) {
         assert.is(scriptOptions.length + styleOptions.length,
                   allOptions.length,
                   'split');
       });
     });
   });
 };
 
 exports.testAllPredictions3 = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping checks due to jsdom document.stylsheets support.');
     return;
   }
 
-  var res1 = types.getType({ name: 'resource' });
+  var res1 = types.createType({ name: 'resource' });
   return res1.getLookup().then(function(options1) {
-    var res2 = types.getType('resource');
+    var res2 = types.createType('resource');
     return res2.getLookup().then(function(options2) {
       assert.is(options1.length, options2.length, 'type spec');
     });
   });
 };
 
 function checkPrediction(res, prediction) {
   var name = prediction.name;
--- a/browser/devtools/commandline/test/browser_gcli_split.js
+++ b/browser/devtools/commandline/test/browser_gcli_split.js
@@ -31,68 +31,69 @@ function test() {
   }).then(finish);
 }
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var assert = require('test/assert');
+var cli = require('gcli/cli');
 var Requisition = require('gcli/cli').Requisition;
 var canon = require('gcli/canon');
 // var mockCommands = require('gclitest/mockCommands');
 
 exports.setup = function(options) {
   mockCommands.setup();
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
 };
 
 
 exports.testSplitSimple = function(options) {
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('s');
-  requ._split(args);
+  args = cli.tokenize('s');
+  requisition._split(args);
   assert.is(0, args.length);
-  assert.is('s', requ.commandAssignment.arg.text);
+  assert.is('s', requisition.commandAssignment.arg.text);
 };
 
 exports.testFlatCommand = function(options) {
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('tsv');
-  requ._split(args);
+  args = cli.tokenize('tsv');
+  requisition._split(args);
   assert.is(0, args.length);
-  assert.is('tsv', requ.commandAssignment.value.name);
+  assert.is('tsv', requisition.commandAssignment.value.name);
 
-  args = requ._tokenize('tsv a b');
-  requ._split(args);
-  assert.is('tsv', requ.commandAssignment.value.name);
+  args = cli.tokenize('tsv a b');
+  requisition._split(args);
+  assert.is('tsv', requisition.commandAssignment.value.name);
   assert.is(2, args.length);
   assert.is('a', args[0].text);
   assert.is('b', args[1].text);
 };
 
 exports.testJavascript = function(options) {
   if (!canon.getCommand('{')) {
     assert.log('Skipping testJavascript because { is not registered');
     return;
   }
 
   var args;
-  var requ = new Requisition();
+  var requisition = new Requisition();
 
-  args = requ._tokenize('{');
-  requ._split(args);
+  args = cli.tokenize('{');
+  requisition._split(args);
   assert.is(1, args.length);
   assert.is('', args[0].text);
-  assert.is('', requ.commandAssignment.arg.text);
-  assert.is('{', requ.commandAssignment.value.name);
+  assert.is('', requisition.commandAssignment.arg.text);
+  assert.is('{', requisition.commandAssignment.value.name);
 };
 
 // BUG 663081 - add tests for sub commands
 
 // });
--- a/browser/devtools/commandline/test/browser_gcli_tokenize.js
+++ b/browser/devtools/commandline/test/browser_gcli_tokenize.js
@@ -31,172 +31,167 @@ function test() {
   }).then(finish);
 }
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var assert = require('test/assert');
-var Requisition = require('gcli/cli').Requisition;
+var cli = require('gcli/cli');
 
 exports.testBlanks = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('');
+  args = cli.tokenize('');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
 
-  args = requ._tokenize(' ');
+  args = cli.tokenize(' ');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is(' ', args[0].prefix);
   assert.is('', args[0].suffix);
 };
 
 exports.testTokSimple = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('s');
+  args = cli.tokenize('s');
   assert.is(1, args.length);
   assert.is('s', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
-  args = requ._tokenize('s s');
+  args = cli.tokenize('s s');
   assert.is(2, args.length);
   assert.is('s', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
   assert.is('s', args[1].text);
   assert.is(' ', args[1].prefix);
   assert.is('', args[1].suffix);
   assert.is('Argument', args[1].type);
 };
 
 exports.testJavascript = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{x}');
+  args = cli.tokenize('{x}');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{ x }');
+  args = cli.tokenize('{ x }');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{ ', args[0].prefix);
   assert.is(' }', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{x} {y}');
+  args = cli.tokenize('{x} {y}');
   assert.is(2, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
   assert.is('y', args[1].text);
   assert.is(' {', args[1].prefix);
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
-  args = requ._tokenize('{x}{y}');
+  args = cli.tokenize('{x}{y}');
   assert.is(2, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
   assert.is('y', args[1].text);
   assert.is('{', args[1].prefix);
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
-  args = requ._tokenize('{');
+  args = cli.tokenize('{');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{ ');
+  args = cli.tokenize('{ ');
   assert.is(1, args.length);
   assert.is('', args[0].text);
   assert.is('{ ', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{x');
+  args = cli.tokenize('{x');
   assert.is(1, args.length);
   assert.is('x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 };
 
 exports.testRegularNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{"x"}');
+  args = cli.tokenize('{"x"}');
   assert.is(1, args.length);
   assert.is('"x"', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{\'x\'}');
+  args = cli.tokenize('{\'x\'}');
   assert.is(1, args.length);
   assert.is('\'x\'', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('"{x}"');
+  args = cli.tokenize('"{x}"');
   assert.is(1, args.length);
   assert.is('{x}', args[0].text);
   assert.is('"', args[0].prefix);
   assert.is('"', args[0].suffix);
   assert.is('Argument', args[0].type);
 
-  args = requ._tokenize('\'{x}\'');
+  args = cli.tokenize('\'{x}\'');
   assert.is(1, args.length);
   assert.is('{x}', args[0].text);
   assert.is('\'', args[0].prefix);
   assert.is('\'', args[0].suffix);
   assert.is('Argument', args[0].type);
 };
 
 exports.testDeepNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('{{}}');
+  args = cli.tokenize('{{}}');
   assert.is(1, args.length);
   assert.is('{}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{{x} {y}}');
+  args = cli.tokenize('{{x} {y}}');
   assert.is(1, args.length);
   assert.is('{x} {y}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
-  args = requ._tokenize('{{w} {{{x}}}} {y} {{{z}}}');
+  args = cli.tokenize('{{w} {{{x}}}} {y} {{{z}}}');
 
   assert.is(3, args.length);
 
   assert.is('{w} {{{x}}}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
@@ -205,65 +200,63 @@ exports.testDeepNesting = function(optio
   assert.is('}', args[1].suffix);
   assert.is('ScriptArgument', args[1].type);
 
   assert.is('{{z}}', args[2].text);
   assert.is(' {', args[2].prefix);
   assert.is('}', args[2].suffix);
   assert.is('ScriptArgument', args[2].type);
 
-  args = requ._tokenize('{{w} {{{x}}} {y} {{{z}}}');
+  args = cli.tokenize('{{w} {{{x}}} {y} {{{z}}}');
 
   assert.is(1, args.length);
 
   assert.is('{w} {{{x}}} {y} {{{z}}}', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 };
 
 exports.testStrangeNesting = function(options) {
   var args;
-  var requ = new Requisition();
 
   // Note: When we get real JS parsing this should break
-  args = requ._tokenize('{"x}"}');
+  args = cli.tokenize('{"x}"}');
 
   assert.is(2, args.length);
 
   assert.is('"x', args[0].text);
   assert.is('{', args[0].prefix);
   assert.is('}', args[0].suffix);
   assert.is('ScriptArgument', args[0].type);
 
   assert.is('}', args[1].text);
   assert.is('"', args[1].prefix);
   assert.is('', args[1].suffix);
   assert.is('Argument', args[1].type);
 };
 
 exports.testComplex = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize(' 1234  \'12 34\'');
+  args = cli.tokenize(' 1234  \'12 34\'');
 
   assert.is(2, args.length);
 
   assert.is('1234', args[0].text);
   assert.is(' ', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
   assert.is('12 34', args[1].text);
   assert.is('  \'', args[1].prefix);
   assert.is('\'', args[1].suffix);
   assert.is('Argument', args[1].type);
 
-  args = requ._tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
+  args = cli.tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \
 
   assert.is(3, args.length);
 
   assert.is('12\'34', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
@@ -275,19 +268,18 @@ exports.testComplex = function(options) 
   assert.is('\\', args[2].text);
   assert.is(' ', args[2].prefix);
   assert.is('', args[2].suffix);
   assert.is('Argument', args[2].type);
 };
 
 exports.testPathological = function(options) {
   var args;
-  var requ = new Requisition();
 
-  args = requ._tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
+  args = cli.tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd
 
   assert.is(4, args.length);
 
   assert.is('a b', args[0].text);
   assert.is('', args[0].prefix);
   assert.is('', args[0].suffix);
   assert.is('Argument', args[0].type);
 
--- a/browser/devtools/commandline/test/browser_gcli_types.js
+++ b/browser/devtools/commandline/test/browser_gcli_types.js
@@ -44,25 +44,32 @@ function forEachType(options, typeSpec, 
     typeSpec.requisition = options.display.requisition;
 
     // Provide some basic defaults to help selection/delegate/array work
     if (name === 'selection') {
       typeSpec.data = [ 'a', 'b' ];
     }
     else if (name === 'delegate') {
       typeSpec.delegateType = function() {
-        return types.getType('string');
+        return types.createType('string');
       };
     }
     else if (name === 'array') {
       typeSpec.subtype = 'string';
     }
 
-    var type = types.getType(typeSpec);
+    var type = types.createType(typeSpec);
     callback(type);
+
+    // Clean up
+    delete typeSpec.name;
+    delete typeSpec.requisition;
+    delete typeSpec.data;
+    delete typeSpec.delegateType;
+    delete typeSpec.subtype;
   });
 }
 
 exports.testDefault = function(options) {
   if (options.isJsdom) {
     assert.log('Skipping tests due to issues with resource type.');
     return;
   }
--- a/browser/devtools/commandline/test/helpers.js
+++ b/browser/devtools/commandline/test/helpers.js
@@ -19,18 +19,17 @@
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
 Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/commandline/test/mockCommands.js
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -24,264 +24,187 @@
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 let { require: require, define: define } = Cu.import("resource://gre/modules/devtools/Require.jsm", {});
 Cu.import("resource:///modules/devtools/gcli.jsm", {});
 
 // <INJECTED SOURCE:END>
 
 var mockCommands = {};
 
+// We use an alias for exports here because this module is used in Firefox
+// mochitests where we don't have define/require
+
+'use strict';
+
 var util = require('util/util');
 var canon = require('gcli/canon');
-
 var types = require('gcli/types');
-var SelectionType = require('gcli/types/selection').SelectionType;
-var DelegateType = require('gcli/types/basic').DelegateType;
-
-
-/**
- * Registration and de-registration.
- */
-mockCommands.setup = function(opts) {
-  // setup/shutdown needs to register/unregister types, however that means we
-  // need to re-initialize mockCommands.option1 and mockCommands.option2 with
-  // the actual types
-  mockCommands.option1.type = types.getType('string');
-  mockCommands.option2.type = types.getType('number');
-
-  types.registerType(mockCommands.optionType);
-  types.registerType(mockCommands.optionValue);
-
-  canon.addCommand(mockCommands.tsv);
-  canon.addCommand(mockCommands.tsr);
-  canon.addCommand(mockCommands.tso);
-  canon.addCommand(mockCommands.tse);
-  canon.addCommand(mockCommands.tsj);
-  canon.addCommand(mockCommands.tsb);
-  canon.addCommand(mockCommands.tss);
-  canon.addCommand(mockCommands.tsu);
-  canon.addCommand(mockCommands.tsf);
-  canon.addCommand(mockCommands.tsn);
-  canon.addCommand(mockCommands.tsnDif);
-  canon.addCommand(mockCommands.tsnExt);
-  canon.addCommand(mockCommands.tsnExte);
-  canon.addCommand(mockCommands.tsnExten);
-  canon.addCommand(mockCommands.tsnExtend);
-  canon.addCommand(mockCommands.tsnDeep);
-  canon.addCommand(mockCommands.tsnDeepDown);
-  canon.addCommand(mockCommands.tsnDeepDownNested);
-  canon.addCommand(mockCommands.tsnDeepDownNestedCmd);
-  canon.addCommand(mockCommands.tselarr);
-  canon.addCommand(mockCommands.tsm);
-  canon.addCommand(mockCommands.tsg);
-  canon.addCommand(mockCommands.tshidden);
-  canon.addCommand(mockCommands.tscook);
-  canon.addCommand(mockCommands.tslong);
-};
 
-mockCommands.shutdown = function(opts) {
-  canon.removeCommand(mockCommands.tsv);
-  canon.removeCommand(mockCommands.tsr);
-  canon.removeCommand(mockCommands.tso);
-  canon.removeCommand(mockCommands.tse);
-  canon.removeCommand(mockCommands.tsj);
-  canon.removeCommand(mockCommands.tsb);
-  canon.removeCommand(mockCommands.tss);
-  canon.removeCommand(mockCommands.tsu);
-  canon.removeCommand(mockCommands.tsf);
-  canon.removeCommand(mockCommands.tsn);
-  canon.removeCommand(mockCommands.tsnDif);
-  canon.removeCommand(mockCommands.tsnExt);
-  canon.removeCommand(mockCommands.tsnExte);
-  canon.removeCommand(mockCommands.tsnExten);
-  canon.removeCommand(mockCommands.tsnExtend);
-  canon.removeCommand(mockCommands.tsnDeep);
-  canon.removeCommand(mockCommands.tsnDeepDown);
-  canon.removeCommand(mockCommands.tsnDeepDownNested);
-  canon.removeCommand(mockCommands.tsnDeepDownNestedCmd);
-  canon.removeCommand(mockCommands.tselarr);
-  canon.removeCommand(mockCommands.tsm);
-  canon.removeCommand(mockCommands.tsg);
-  canon.removeCommand(mockCommands.tshidden);
-  canon.removeCommand(mockCommands.tscook);
-  canon.removeCommand(mockCommands.tslong);
+mockCommands.option1 = { };
+mockCommands.option2 = { };
+mockCommands.option3 = { };
 
-  types.deregisterType(mockCommands.optionType);
-  types.deregisterType(mockCommands.optionValue);
-};
-
-
-mockCommands.option1 = { type: types.getType('string') };
-mockCommands.option2 = { type: types.getType('number') };
-mockCommands.option3 = { type: types.getType({
-  name: 'selection',
-  lookup: [
-    { name: 'one', value: 1 },
-    { name: 'two', value: 2 },
-    { name: 'three', value: 3 }
-  ]
-})};
-
-mockCommands.optionType = new SelectionType({
+mockCommands.optionType = {
   name: 'optionType',
+  parent: 'selection',
   lookup: [
     { name: 'option1', value: mockCommands.option1 },
     { name: 'option2', value: mockCommands.option2 },
     { name: 'option3', value: mockCommands.option3 }
   ]
-});
+};
 
-mockCommands.optionValue = new DelegateType({
+mockCommands.optionValue = {
   name: 'optionValue',
-  delegateType: function(context) {
-    if (context != null) {
-      var option = context.getArgsObject().optionType;
+  parent: 'delegate',
+  delegateType: function(executionContext) {
+    if (executionContext != null) {
+      var option = executionContext.getArgsObject().optionType;
       if (option != null) {
         return option.type;
       }
     }
-    return types.getType('blank');
+    return types.createType('blank');
   }
-});
+};
 
 mockCommands.onCommandExec = util.createEvent('commands.onCommandExec');
 
 function createExec(name) {
-  return function(args, context) {
+  return function(args, executionContext) {
     var data = {
-      command: mockCommands[name],
       args: args,
-      context: context
+      context: executionContext
     };
     mockCommands.onCommandExec(data);
     var argsOut = Object.keys(args).map(function(key) {
       return key + '=' + args[key];
     }).join(', ');
     return 'Exec: ' + name + ' ' + argsOut;
   };
 }
 
-mockCommands.tsv = {
+var tsv = {
   name: 'tsv',
   params: [
     { name: 'optionType', type: 'optionType' },
     { name: 'optionValue', type: 'optionValue' }
   ],
   exec: createExec('tsv')
 };
 
-mockCommands.tsr = {
+var tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsr')
 };
 
-mockCommands.tso = {
+var tso = {
   name: 'tso',
   params: [ { name: 'text', type: 'string', defaultValue: null } ],
   exec: createExec('tso')
 };
 
-mockCommands.tse = {
+var tse = {
   name: 'tse',
   params: [
     { name: 'node', type: 'node' },
     {
       group: 'options',
       params: [
         { name: 'nodes', type: { name: 'nodelist' } },
         { name: 'nodes2', type: { name: 'nodelist', allowEmpty: true } }
       ]
     }
   ],
   exec: createExec('tse')
 };
 
-mockCommands.tsj = {
+var tsj = {
   name: 'tsj',
   params: [ { name: 'javascript', type: 'javascript' } ],
   exec: createExec('tsj')
 };
 
-mockCommands.tsb = {
+var tsb = {
   name: 'tsb',
   params: [ { name: 'toggle', type: 'boolean' } ],
   exec: createExec('tsb')
 };
 
-mockCommands.tss = {
+var tss = {
   name: 'tss',
   exec: createExec('tss')
 };
 
-mockCommands.tsu = {
+var tsu = {
   name: 'tsu',
   params: [ { name: 'num', type: { name: 'number', max: 10, min: -5, step: 3 } } ],
   exec: createExec('tsu')
 };
 
-mockCommands.tsf = {
+var tsf = {
   name: 'tsf',
   params: [ { name: 'num', type: { name: 'number', allowFloat: true, max: 11.5, min: -6.5, step: 1.5 } } ],
   exec: createExec('tsf')
 };
 
-mockCommands.tsn = {
+var tsn = {
   name: 'tsn'
 };
 
-mockCommands.tsnDif = {
+var tsnDif = {
   name: 'tsn dif',
   description: 'tsn dif',
   params: [ { name: 'text', type: 'string', description: 'tsn dif text' } ],
   exec: createExec('tsnDif')
 };
 
-mockCommands.tsnExt = {
+var tsnExt = {
   name: 'tsn ext',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExt')
 };
 
-mockCommands.tsnExte = {
+var tsnExte = {
   name: 'tsn exte',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExte')
 };
 
-mockCommands.tsnExten = {
+var tsnExten = {
   name: 'tsn exten',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExten')
 };
 
-mockCommands.tsnExtend = {
+var tsnExtend = {
   name: 'tsn extend',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsnExtend')
 };
 
-mockCommands.tsnDeep = {
+var tsnDeep = {
   name: 'tsn deep'
 };
 
-mockCommands.tsnDeepDown = {
+var tsnDeepDown = {
   name: 'tsn deep down'
 };
 
-mockCommands.tsnDeepDownNested = {
+var tsnDeepDownNested = {
   name: 'tsn deep down nested'
 };
 
-mockCommands.tsnDeepDownNestedCmd = {
+var tsnDeepDownNestedCmd = {
   name: 'tsn deep down nested cmd',
   exec: createExec('tsnDeepDownNestedCmd')
 };
 
-mockCommands.tshidden = {
+var tshidden = {
   name: 'tshidden',
   hidden: true,
   params: [
     {
       group: 'Options',
       params: [
         {
           name: 'visible',
@@ -303,37 +226,37 @@ mockCommands.tshidden = {
           hidden: true
         }
       ]
     }
   ],
   exec: createExec('tshidden')
 };
 
-mockCommands.tselarr = {
+var tselarr = {
   name: 'tselarr',
   params: [
     { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     { name: 'arr', type: { name: 'array', subtype: 'string' } }
   ],
   exec: createExec('tselarr')
 };
 
-mockCommands.tsm = {
+var tsm = {
   name: 'tsm',
   description: 'a 3-param test selection|string|number',
   params: [
     { name: 'abc', type: { name: 'selection', data: [ 'a', 'b', 'c' ] } },
     { name: 'txt', type: 'string' },
     { name: 'num', type: { name: 'number', max: 42, min: 0 } }
   ],
   exec: createExec('tsm')
 };
 
-mockCommands.tsg = {
+var tsg = {
   name: 'tsg',
   description: 'a param group test',
   params: [
     {
       name: 'solo',
       type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] },
       description: 'solo param'
     },
@@ -366,17 +289,17 @@ mockCommands.tsg = {
       defaultValue: 42,
       description: 'num param',
       option: 'Second'
     }
   ],
   exec: createExec('tsg')
 };
 
-mockCommands.tscook = {
+var tscook = {
   name: 'tscook',
   description: 'param group test to catch problems with cookie command',
   params: [
     {
       name: 'key',
       type: 'string',
       description: 'tscookKeyDesc'
     },
@@ -406,20 +329,19 @@ mockCommands.tscook = {
           description: 'tscookSecureDesc'
         }
       ]
     }
   ],
   exec: createExec('tscook')
 };
 
-mockCommands.tslong = {
+var tslong = {
   name: 'tslong',
   description: 'long param tests to catch problems with the jsb command',
-  returnValue:'string',
   params: [
     {
       name: 'msg',
       type: 'string',
       description: 'msg Desc'
     },
     {
       group: "Options Desc",
@@ -468,10 +390,151 @@ mockCommands.tslong = {
           defaultValue: "collapse"
         }
       ]
     }
   ],
   exec: createExec('tslong')
 };
 
+var tsfail = {
+  name: 'tsfail',
+  description: 'test errors',
+  params: [
+    {
+      name: 'method',
+      type: {
+        name: 'selection',
+        data: [
+          'reject', 'rejecttyped',
+          'throwerror', 'throwstring', 'throwinpromise',
+          'noerror'
+        ]
+      }
+    }
+  ],
+  exec: function(args, context) {
+    if (args.method === 'reject') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.reject('rejected promise');
+      }, 10);
+      return deferred.promise;
+    }
+
+    if (args.method === 'rejecttyped') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.reject(context.typedData('number', 54));
+      }, 10);
+      return deferred.promise;
+    }
+
+    if (args.method === 'throwinpromise') {
+      var deferred = context.defer();
+      setTimeout(function() {
+        deferred.resolve('should be lost');
+      }, 10);
+      return deferred.promise.then(function() {
+        var t = null;
+        return t.foo;
+      });
+    }
+
+    if (args.method === 'throwerror') {
+      throw new Error('thrown error');
+    }
+
+    if (args.method === 'throwstring') {
+      throw 'thrown string';
+    }
+
+    return 'no error';
+  }
+};
+
+mockCommands.commands = {};
+
+/**
+ * Registration and de-registration.
+ */
+mockCommands.setup = function(opts) {
+  // setup/shutdown needs to register/unregister types, however that means we
+  // need to re-initialize mockCommands.option1 and mockCommands.option2 with
+  // the actual types
+  mockCommands.option1.type = types.createType('string');
+  mockCommands.option2.type = types.createType('number');
+  mockCommands.option3.type = types.createType({
+    name: 'selection',
+    lookup: [
+      { name: 'one', value: 1 },
+      { name: 'two', value: 2 },
+      { name: 'three', value: 3 }
+    ]
+  });
+
+  types.addType(mockCommands.optionType);
+  types.addType(mockCommands.optionValue);
+
+  mockCommands.commands.tsv = canon.addCommand(tsv);
+  mockCommands.commands.tsr = canon.addCommand(tsr);
+  mockCommands.commands.tso = canon.addCommand(tso);
+  mockCommands.commands.tse = canon.addCommand(tse);
+  mockCommands.commands.tsj = canon.addCommand(tsj);
+  mockCommands.commands.tsb = canon.addCommand(tsb);
+  mockCommands.commands.tss = canon.addCommand(tss);
+  mockCommands.commands.tsu = canon.addCommand(tsu);
+  mockCommands.commands.tsf = canon.addCommand(tsf);
+  mockCommands.commands.tsn = canon.addCommand(tsn);
+  mockCommands.commands.tsnDif = canon.addCommand(tsnDif);
+  mockCommands.commands.tsnExt = canon.addCommand(tsnExt);
+  mockCommands.commands.tsnExte = canon.addCommand(tsnExte);
+  mockCommands.commands.tsnExten = canon.addCommand(tsnExten);
+  mockCommands.commands.tsnExtend = canon.addCommand(tsnExtend);
+  mockCommands.commands.tsnDeep = canon.addCommand(tsnDeep);
+  mockCommands.commands.tsnDeepDown = canon.addCommand(tsnDeepDown);
+  mockCommands.commands.tsnDeepDownNested = canon.addCommand(tsnDeepDownNested);
+  mockCommands.commands.tsnDeepDownNestedCmd = canon.addCommand(tsnDeepDownNestedCmd);
+  mockCommands.commands.tselarr = canon.addCommand(tselarr);
+  mockCommands.commands.tsm = canon.addCommand(tsm);
+  mockCommands.commands.tsg = canon.addCommand(tsg);
+  mockCommands.commands.tshidden = canon.addCommand(tshidden);
+  mockCommands.commands.tscook = canon.addCommand(tscook);
+  mockCommands.commands.tslong = canon.addCommand(tslong);
+  mockCommands.commands.tsfail = canon.addCommand(tsfail);
+};
+
+mockCommands.shutdown = function(opts) {
+  canon.removeCommand(tsv);
+  canon.removeCommand(tsr);
+  canon.removeCommand(tso);
+  canon.removeCommand(tse);
+  canon.removeCommand(tsj);
+  canon.removeCommand(tsb);
+  canon.removeCommand(tss);
+  canon.removeCommand(tsu);
+  canon.removeCommand(tsf);
+  canon.removeCommand(tsn);
+  canon.removeCommand(tsnDif);
+  canon.removeCommand(tsnExt);
+  canon.removeCommand(tsnExte);
+  canon.removeCommand(tsnExten);
+  canon.removeCommand(tsnExtend);
+  canon.removeCommand(tsnDeep);
+  canon.removeCommand(tsnDeepDown);
+  canon.removeCommand(tsnDeepDownNested);
+  canon.removeCommand(tsnDeepDownNestedCmd);
+  canon.removeCommand(tselarr);
+  canon.removeCommand(tsm);
+  canon.removeCommand(tsg);
+  canon.removeCommand(tshidden);
+  canon.removeCommand(tscook);
+  canon.removeCommand(tslong);
+  canon.removeCommand(tsfail);
+
+  types.removeType(mockCommands.optionType);
+  types.removeType(mockCommands.optionValue);
+
+  mockCommands.commands = {};
+};
+
 
 // });
--- a/browser/devtools/debugger/CmdDebugger.jsm
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -152,17 +152,17 @@ gcli.addCommand({
       description: gcli.lookup("breakaddlineFileDesc")
     },
     {
       name: "line",
       type: { name: "number", min: 1, step: 10 },
       description: gcli.lookup("breakaddlineLineDesc")
     }
   ],
-  returnType: "html",
+  returnType: "string",
   exec: function(args, context) {
     args.type = "line";
 
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
       return gcli.lookup("debuggerStopped");
     }
     var deferred = context.defer();
@@ -196,17 +196,17 @@ gcli.addCommand({
           return dbg == null ?
               null :
               Object.keys(dbg.getAllBreakpoints()).length - 1;
         },
       },
       description: gcli.lookup("breakdelBreakidDesc")
     }
   ],
-  returnType: "html",
+  returnType: "string",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
       return gcli.lookup("debuggerStopped");
     }
 
     let breakpoints = dbg.getAllBreakpoints();
     let id = Object.keys(breakpoints)[args.breakid];
@@ -376,17 +376,17 @@ gcli.addCommand({
 
 /**
  * 'dbg list' command
  */
 gcli.addCommand({
   name: "dbg list",
   description: gcli.lookup("dbgListSourcesDesc"),
   params: [],
-  returnType: "html",
+  returnType: "dom",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     let doc = context.environment.chromeDocument;
     if (!dbg) {
       return gcli.lookup("debuggerClosed");
     }
     let sources = dbg._view.Sources.values;
     let div = createXHTMLElement(doc, "div");
--- a/browser/devtools/debugger/test/helpers.js
+++ b/browser/devtools/debugger/test/helpers.js
@@ -19,18 +19,17 @@
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
 Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/inspector/test/helpers.js
+++ b/browser/devtools/inspector/test/helpers.js
@@ -19,18 +19,17 @@
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
 Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/responsivedesign/test/helpers.js
+++ b/browser/devtools/responsivedesign/test/helpers.js
@@ -19,18 +19,17 @@
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
 Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -148,17 +148,17 @@ let CommandUtils = {
 
   /**
    * A helper function to create the environment object that is passed to
    * GCLI commands.
    */
   createEnvironment: function(chromeDocument, contentDocument) {
     let environment = {
       chromeDocument: chromeDocument,
-      contentDocument: contentDocument, // Use of contentDocument is deprecated
+      chromeWindow: chromeDocument.defaultView,
 
       document: contentDocument,
       window: contentDocument.defaultView
     };
 
     Object.defineProperty(environment, "target", {
       get: function() {
         let tab = chromeDocument.defaultView.getBrowser().selectedTab;
@@ -756,16 +756,17 @@ function OutputPanel(aDevToolbar, aLoadC
   this._frame.id = "gcli-output-frame";
   this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml");
   this._frame.setAttribute("sandbox", "allow-same-origin");
   this._panel.appendChild(this._frame);
 
   this.displayedOutput = undefined;
 
   this._onload = this._onload.bind(this);
+  this._update = this._update.bind(this);
   this._frame.addEventListener("load", this._onload, true);
 
   this.loaded = false;
 }
 
 /**
  * Wire up the element from the iframe, and inform the _loadCallback.
  */
@@ -901,38 +902,48 @@ OutputPanel.prototype._outputChanged = f
 {
   if (aEvent.output.hidden) {
     return;
   }
 
   this.remove();
 
   this.displayedOutput = aEvent.output;
-  this.update();
+  this.displayedOutput.onClose.add(this.remove, this);
 
-  this.displayedOutput.onChange.add(this.update, this);
-  this.displayedOutput.onClose.add(this.remove, this);
+  if (this.displayedOutput.completed) {
+    this._update();
+  }
+  else {
+    this.displayedOutput.promise.then(this._update, this._update)
+                                .then(null, console.error);
+  }
 };
 
 /**
  * Called when displayed Output says it's changed or from outputChanged, which
  * happens when there is a new displayed Output.
  */
-OutputPanel.prototype.update = function OP_update()
+OutputPanel.prototype._update = function OP_update()
 {
+  // destroy has been called, bail out
+  if (this._div == null) {
+    return;
+  }
+
   // Empty this._div
   while (this._div.hasChildNodes()) {
     this._div.removeChild(this._div.firstChild);
   }
 
   if (this.displayedOutput.data != null) {
     let requisition = this._devtoolbar.display.requisition;
     let nodePromise = converters.convert(this.displayedOutput.data,
                                          this.displayedOutput.type, 'dom',
-                                         requisition.context);
+                                         requisition.conversionContext);
     nodePromise.then(function(node) {
       while (this._div.hasChildNodes()) {
         this._div.removeChild(this._div.firstChild);
       }
 
       var links = node.ownerDocument.querySelectorAll('*[href]');
       for (var i = 0; i < links.length; i++) {
         links[i].setAttribute('target', '_blank');
@@ -953,17 +964,16 @@ OutputPanel.prototype.remove = function 
     this.canHide = true;
   }
 
   if (this._panel && this._panel.hidePopup) {
     this._panel.hidePopup();
   }
 
   if (this.displayedOutput) {
-    this.displayedOutput.onChange.remove(this.update, this);
     this.displayedOutput.onClose.remove(this.remove, this);
     delete this.displayedOutput;
   }
 };
 
 /**
  * Detach listeners from the currently displayed Output.
  */
--- a/browser/devtools/styleeditor/test/helpers.js
+++ b/browser/devtools/styleeditor/test/helpers.js
@@ -19,18 +19,17 @@
 
 this.EXPORTED_SYMBOLS = [ 'helpers' ];
 var helpers = {};
 this.helpers = helpers;
 let require = (Cu.import("resource://gre/modules/devtools/Require.jsm", {})).require;
 Components.utils.import("resource:///modules/devtools/gcli.jsm", {});
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
-let devtools = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools;
-let TargetFactory = devtools.TargetFactory;
+let TargetFactory = (Cu.import("resource:///modules/devtools/gDevTools.jsm", {})).devtools.TargetFactory;
 
 let Promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise;
 let assert = { ok: ok, is: is, log: info };
 
 var util = require('util/util');
 
 var converters = require('gcli/converters');
 
@@ -153,16 +152,17 @@ helpers.addTabWithToolbar = function(url
  * their return values resolved
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
+    ok(false, error);
     console.error(error);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
@@ -336,17 +336,17 @@ helpers._createDebugCheck = function(opt
 
     output += '      input:  \'' + input + '\',\n';
     output += '      hints:  ' + padding + '\'' + hints + '\',\n';
     output += '      markup: \'' + helpers._actual.markup(options) + '\',\n';
     output += '      cursor: ' + cursor + ',\n';
     output += '      current: \'' + helpers._actual.current(options) + '\',\n';
     output += '      status: \'' + helpers._actual.status(options) + '\',\n';
     output += '      options: ' + outputArray(helpers._actual.options(options)) + ',\n';
-    output += '      error: \'' + helpers._actual.message(options) + '\',\n';
+    output += '      message: \'' + helpers._actual.message(options) + '\',\n';
     output += '      predictions: ' + outputArray(predictions) + ',\n';
     output += '      unassigned: ' + outputArray(requisition._unassigned) + ',\n';
     output += '      outputState: \'' + helpers._actual.outputState(options) + '\',\n';
     output += '      tooltipState: \'' + helpers._actual.tooltipState(options) + '\'' +
               (command ? ',' : '') +'\n';
 
     if (command) {
       output += '      args: {\n';
@@ -373,16 +373,18 @@ helpers._createDebugCheck = function(opt
 
       output += '      }\n';
     }
 
     output += '    },\n';
     output += '    exec: {\n';
     output += '      output: \'\',\n';
     output += '      completed: true,\n';
+    output += '      type: \'string\',\n';
+    output += '      error: false\n';
     output += '    }\n';
     output += '  }\n';
     output += ']);';
 
     return output;
   }.bind(this), util.errorHandler);
 };
 
@@ -697,43 +699,55 @@ helpers._check = function(options, name,
  * Helper for helpers.audit() to ensure that all the 'exec' properties work.
  * See helpers.audit for more information.
  * @param name The name to use in error messages
  * @param expected See helpers.audit for a list of available exec checks
  * @return A promise which resolves to undefined when the checks are complete
  */
 helpers._exec = function(options, name, expected) {
   if (expected == null) {
-    return Promise.resolve();
+    return Promise.resolve({});
   }
 
   var output = options.display.requisition.exec({ hidden: true });
 
   if ('completed' in expected) {
     assert.is(output.completed,
               expected.completed,
               'output.completed false for: ' + name);
   }
 
   if (!options.window.document.createElement) {
     assert.log('skipping output tests (missing doc.createElement) for ' + name);
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
-    return Promise.resolve();
+    return Promise.resolve({ output: output });
   }
 
-  var deferred = Promise.defer();
-
   var checkOutput = function() {
     var div = options.window.document.createElement('div');
-    var nodePromise = converters.convert(output.data, output.type, 'dom',
-                                         options.display.requisition.context);
-    nodePromise.then(function(node) {
+    var conversionContext = options.display.requisition.conversionContext;
+
+    if ('type' in expected) {
+      assert.is(output.type,
+                expected.type,
+                'output.type for: ' + name);
+    }
+
+    if ('error' in expected) {
+      assert.is(output.error,
+                expected.error,
+                'output.error for: ' + name);
+    }
+
+    var convertPromise = converters.convert(output.data, output.type, 'dom',
+                                            conversionContext);
+    return convertPromise.then(function(node) {
       div.appendChild(node);
       var actualOutput = div.textContent.trim();
 
       var doTest = function(match, against) {
         if (match.test(against)) {
           assert.ok(true, 'html output for ' + name + ' should match ' +
                           match.source);
         } else {
@@ -752,57 +766,44 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
-      deferred.resolve(actualOutput);
+      return { output: output, text: actualOutput };
     });
   };
 
-  if (output.completed !== false) {
-    checkOutput();
-  }
-  else {
-    var changed = function() {
-      if (output.completed !== false) {
-        checkOutput();
-        output.onChange.remove(changed);
-      }
-    };
-    output.onChange.add(changed);
-  }
-
-  return deferred.promise;
+  return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
  * Helper to setup the test
  */
 helpers._setup = function(options, name, action) {
   if (typeof action === 'string') {
     return helpers.setInput(options, action);
   }
 
   if (typeof action === 'function') {
     return Promise.resolve(action());
   }
 
-  return Promise.reject('setup must be a string or a function');
+  return Promise.reject('\'setup\' property must be a string or a function. Is ' + action);
 };
 
 /**
  * Helper to shutdown the test
  */
-helpers._post = function(name, action, output) {
+helpers._post = function(name, action, data) {
   if (typeof action === 'function') {
-    return Promise.resolve(action(output));
+    return Promise.resolve(action(data.output, data.text));
   }
   return Promise.resolve(action);
 };
 
 /*
  * We do some basic response time stats so we can see if we're getting slow
  */
 var totalResponseTime = 0;
@@ -938,39 +939,41 @@ helpers.audit = function(options, audits
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
     return setupDone.then(function(chunkLen) {
 
       if (typeof chunkLen !== 'number') {
         chunkLen = 1;
       }
-      var responseTime = (new Date().getTime() - start) / chunkLen;
-      totalResponseTime += responseTime;
-      if (responseTime > maxResponseTime) {
-        maxResponseTime = responseTime;
-        maxResponseCulprit = assert.currentTest + '/' + name;
+
+      if (assert.currentTest) {
+        var responseTime = (new Date().getTime() - start) / chunkLen;
+        totalResponseTime += responseTime;
+        if (responseTime > maxResponseTime) {
+          maxResponseTime = responseTime;
+          maxResponseCulprit = assert.currentTest + '/' + name;
+        }
+        averageOver++;
       }
-      averageOver++;
 
       var checkDone = helpers._check(options, name, audit.check);
       return checkDone.then(function() {
         var execDone = helpers._exec(options, name, audit.exec);
-        return execDone.then(function(output) {
-          return helpers._post(name, audit.post, output).then(function() {
+        return execDone.then(function(data) {
+          return helpers._post(name, audit.post, data).then(function() {
             if (assert.testLogging) {
               log('- END \'' + name + '\' in ' + assert.currentTest);
             }
           });
         });
       });
     });
-  }).then(null, function(ex) {
-    console.error(ex.stack);
-    throw(ex);
+  }).then(function() {
+    return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
 helpers._arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -14,16 +14,30 @@
 # LOCALIZATION NOTE (canonDescNone): Short string used to describe any command
 # or command parameter when no description has been provided.
 canonDescNone=(No description)
 
 # LOCALIZATION NOTE (canonDefaultGroupName): The default name for a group of
 # parameters.
 canonDefaultGroupName=Options
 
+# LOCALIZATION NOTE (canonProxyDesc): A very short description of a set of
+# remote commands. This string is designed to be shown in a menu alongside the
+# command name, which is why it should be as short as possible. See
+# canonProxyManual for a fuller description of what it does.
+canonProxyDesc=Execute a command on %S
+
+# LOCALIZATION NOTE (canonProxyManual): A fuller description of a set of
+# remote commands. Displayed when the user asks for help on what it does.
+canonProxyManual=A set of commands that are executed on a remote system. The remote system is reached via %S
+
+# LOCALIZATION NOTE (canonProxyExists): An error message displayed when we try
+# to add new command (via a proxy) where one already exists in that name.
+canonProxyExists=There is already a command called '%S'
+
 # LOCALIZATION NOTE (cliEvalJavascript): The special '{' command allows entry
 # of JavaScript like traditional developer tool command lines. This describes
 # the '{' command.
 cliEvalJavascript=Enter JavaScript directly
 
 # LOCALIZATION NOTE (cliUnusedArg): When the command line has more arguments
 # than the current command can understand this is the error message shown to
 # the user.
@@ -185,16 +199,102 @@ helpManOptional=optional
 # of the matching sub-commands
 subCommands=Sub-Commands
 
 # LOCALIZATION NOTE (subCommandsNone): Text shown as part of the output of the
 # 'help' command when the command in question should have sub-commands but in
 # fact has none
 subCommandsNone=None
 
+# LOCALIZATION NOTE (contextDesc): A very short description of the 'context'
+# command. This string is designed to be shown in a menu alongside the command
+# name, which is why it should be as short as possible. See contextManual for
+# a fuller description of what it does.
+contextDesc=Concentrate on a group of commands
+
+# LOCALIZATION NOTE (contextManual): A fuller description of the 'context'
+# command. Displayed when the user asks for help on what it does.
+contextManual=Setup a default prefix to future commands. For example 'context git' would allow you to type 'commit' rather than 'git commit'.
+
+# LOCALIZATION NOTE (contextPrefixDesc): A short description of the 'prefix'
+# parameter to the 'context' command. This string is designed to be shown in a
+# dialog with restricted space, which is why it should be as short as
+# possible.
+contextPrefixDesc=The command prefix
+
+# LOCALIZATION NOTE (contextNotParentError): An error message displayed during
+# the processing of the 'context' command, when the found command is not a
+# parent command.
+contextNotParentError=Can't use '%1$S' as a prefix because it is not a parent command.
+
+# LOCALIZATION NOTE (contextReply): A message displayed during the processing
+# of the 'context' command, to indicate success.
+contextReply=Using %1$S as a command prefix
+
+# LOCALIZATION NOTE (contextEmptyReply): A message displayed during the
+# processing of the 'context' command, to indicate that there is no command
+# prefix
+contextEmptyReply=Command prefix is unset
+
+# LOCALIZATION NOTE (connectDesc): A very short description of the 'connect'
+# command. This string is designed to be shown in a menu alongside the command
+# name, which is why it should be as short as possible. See connectManual for
+# a fuller description of what it does.
+connectDesc=Proxy commands to server
+
+# LOCALIZATION NOTE (connectManual): A fuller description of the 'connect'
+# command. Displayed when the user asks for help on what it does.
+connectManual=Connect to the server, creating local versions of the commands on the server. Remote commands initially have a prefix to distinguish them from local commands (but see the context command to get past this)
+
+# LOCALIZATION NOTE (connectPrefixDesc): A short description of the 'prefix'
+# parameter to the 'connect' command. This string is designed to be shown in a
+# dialog with restricted space, which is why it should be as short as
+# possible.
+connectPrefixDesc=Parent prefix for imported commands
+
+# LOCALIZATION NOTE (connectPortDesc): A short description of the 'port'
+# parameter to the 'connect' command. This string is designed to be shown in a
+# dialog with restricted space, which is why it should be as short as
+# possible.
+connectPortDesc=The TCP port to listen on
+
+# LOCALIZATION NOTE (connectHostDesc): A short description of the 'host'
+# parameter to the 'connect' command. This string is designed to be shown in a
+# dialog with restricted space, which is why it should be as short as
+# possible.
+connectHostDesc=The hostname to bind to
+
+# LOCALIZATION NOTE (connectDupReply): An error condition from executing the
+# 'connect' command
+connectDupReply=Connection called %S already exists.
+
+# LOCALIZATION NOTE (connectReply): The output of the 'connect' command,
+# telling the user what it has done.
+connectReply=Added %S commands.
+
+# LOCALIZATION NOTE (disconnectDesc): A very short description of the
+# 'disconnect' command. This string is designed to be shown in a menu
+# alongside the command name, which is why it should be as short as possible.
+# See connectManual for a fuller description of what it does.
+disconnectDesc=Proxy commands to server
+
+# LOCALIZATION NOTE (disconnectManual): A fuller description of the
+# 'disconnect' command. Displayed when the user asks for help on what it does.
+disconnectManual=Connect to the server, creating local versions of the commands on the server. Remote commands initially have a prefix to distinguish them from local commands (but see the context command to get past this)
+
+# LOCALIZATION NOTE (disconnectPrefixDesc): A short description of the
+# 'prefix' parameter to the 'disconnect' command. This string is designed to
+# be shown in a dialog with restricted space, which is why it should be as
+# short as possible.
+disconnectPrefixDesc=Parent prefix for imported commands
+
+# LOCALIZATION NOTE (disconnectReply): The output of the 'disconnect' command,
+# telling the user what it's done.
+disconnectReply=Removed %S commands.
+
 # LOCALIZATION NOTE (prefDesc): A very short description of the 'pref'
 # command. This string is designed to be shown in a menu alongside the command
 # name, which is why it should be as short as possible. See prefManual for a
 # fuller description of what it does.
 prefDesc=Commands to control settings
 
 # LOCALIZATION NOTE (prefManual): A fuller description of the 'pref' command.
 # Displayed when the user asks for help on what it does.
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -7,24 +7,16 @@
 # -> 'Web Console'.
 #
 # The correct localization of this file might be to keep it in
 # English, or another language commonly spoken among web developers.
 # You want to make that choice consistent across the developer tools.
 # A good criteria is the language in which you'd find the best
 # documentation on web development on the web.
 
-# LOCALIZATION NOTE (echoDesc) A very short string used to describe the
-# function of the echo command.
-echoDesc=Show a message
-
-# LOCALIZATION NOTE (echoMessageDesc) A very short string used to describe the
-# message parameter to the echo command.
-echoMessageDesc=Message
-
 # LOCALIZATION NOTE (helpDesc) A very short string used to describe the
 # function of the help command.
 helpDesc=Get help on the available commands
 
 # LOCALIZATION NOTE (helpAvailable) Used in the output of the help command to
 # explain the contents of the command help table.
 helpAvailable=Available Commands
 
@@ -1264,8 +1256,32 @@ profilerStarting2=Starting…
 
 # LOCALIZATION NOTE (profilerStopping) A very short string that indicates that
 # we're stopping the profiler.
 profilerStopping2=Stopping…
 
 # LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever
 # an operation cannot be completed because the profiler has not been opened yet.
 profilerNotReady=For this command to work you need to open the profiler first
+
+# LOCALIZATION NOTE (listenDesc) A very short string used to describe the
+# function of the 'listen' command.
+listenDesc=Open a remote debug port
+
+# LOCALIZATION NOTE (listenManual) A longer description of the 'listen'
+# command.
+listenManual=Firefox can allow remote debugging over a TCP/IP connection. For security reasons this is turned off by default, but can be enabled using this command.
+
+# LOCALIZATION NOTE (listenPortDesc) A very short string used to describe the
+# function of 'port' parameter to the 'listen' command.
+listenPortDesc=The TCP port to listen on
+
+# LOCALIZATION NOTE (listenDisabledOutput) Text of a message output during the
+# execution of the 'listen' command.
+listenDisabledOutput=Listen is disabled by the devtools.debugger.remote-enabled preference
+
+# LOCALIZATION NOTE (listenInitOutput) Text of a message output during the
+# execution of the 'listen' command. %1$S is a port number
+listenInitOutput=Listening on port %1$S
+
+# LOCALIZATION NOTE (listenNoInitOutput) Text of a message output during the
+# execution of the 'listen' command.
+listenNoInitOutput=DebuggerServer not initialized
--- a/toolkit/devtools/debugger/server/dbg-server.js
+++ b/toolkit/devtools/debugger/server/dbg-server.js
@@ -177,16 +177,20 @@ var DebuggerServer = {
    * Install Firefox-specific actors.
    */
   addBrowserActors: function DS_addBrowserActors() {
     this.addActors("chrome://global/content/devtools/dbg-browser-actors.js");
 #ifndef MOZ_WIDGET_GONK
     this.addActors("chrome://global/content/devtools/dbg-webconsole-actors.js");
     this.addTabActor(this.WebConsoleActor, "consoleActor");
     this.addGlobalActor(this.WebConsoleActor, "consoleActor");
+
+    this.addActors("chrome://global/content/devtools/dbg-gcli-actors.js");
+    this.addTabActor(this.GcliActor, "gcliActor");
+    this.addGlobalActor(this.GcliActor, "gcliActor");
 #endif
     if ("nsIProfiler" in Ci)
       this.addActors("chrome://global/content/devtools/dbg-profiler-actors.js");
 
     this.addActors("chrome://global/content/devtools/dbg-styleeditor-actors.js");
     this.addTabActor(this.StyleEditorActor, "styleEditorActor");
   },
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/gcli/Makefile.in
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH		= @DEPTH@
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/gcli/dbg-gcli-actors.js
@@ -0,0 +1,66 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+let { require } = Cu.import("resource://gre/modules/devtools/Require.jsm", {});
+Cu.import("resource:///modules/devtools/gcli.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource://gre/modules/devtools/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CommandUtils",
+                                  "resource:///modules/devtools/DeveloperToolbar.jsm");
+
+let canon = require("gcli/canon");
+let Requisition = require("gcli/cli").Requisition;
+let util = require("util/util");
+
+/**
+ * Manage remote connections that want to talk to GCLI
+ * @constructor
+ * @param connection The connection to the client, DebuggerServerConnection
+ * @param parentActor Optional, the parent actor
+ */
+function GcliActor(connection, parentActor) {
+  this.connection = connection;
+}
+
+GcliActor.prototype.actorPrefix = "gcli";
+
+GcliActor.prototype.disconnect = function() {
+};
+
+GcliActor.prototype.getCommandSpecs = function(request) {
+  return { commandSpecs: canon.getCommandSpecs() };
+};
+
+GcliActor.prototype.execute = function(request) {
+  let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
+                        .getService(Ci.nsIWindowMediator);
+  let chromeWindow = windowMediator.getMostRecentWindow("navigator:browser");
+  let contentWindow = chromeWindow.gBrowser.selectedTab.linkedBrowser.contentWindow;
+
+  let environment = CommandUtils.createEnvironment(chromeWindow.document,
+                                                   contentWindow.document);
+
+  let requisition = new Requisition(environment);
+  let outputPromise = requisition.updateExec(request.typed);
+  let output = util.synchronize(outputPromise);
+
+  return {
+    id: request.id,
+    data: output.data,
+    type: output.type,
+    error: output.error
+  };
+};
+
+GcliActor.prototype.requestTypes = {
+  getCommandSpecs: GcliActor.prototype.getCommandSpecs,
+  execute: GcliActor.prototype.execute,
+};
--- a/toolkit/devtools/jar.mn
+++ b/toolkit/devtools/jar.mn
@@ -2,11 +2,12 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
   content/global/devtools/dbg-transport.js        (debugger/dbg-transport.js)
 * content/global/devtools/dbg-server.js           (debugger/server/dbg-server.js)
   content/global/devtools/dbg-script-actors.js    (debugger/server/dbg-script-actors.js)
   content/global/devtools/dbg-browser-actors.js   (debugger/server/dbg-browser-actors.js)
+  content/global/devtools/dbg-gcli-actors.js      (gcli/dbg-gcli-actors.js)
   content/global/devtools/dbg-webconsole-actors.js (webconsole/dbg-webconsole-actors.js)
   content/global/devtools/dbg-profiler-actors.js  (debugger/server/dbg-profiler-actors.js)
   content/global/devtools/dbg-styleeditor-actors.js  (styleeditor/dbg-styleeditor-actors.js)