merge m-c to fx-team
authorTim Taubert <ttaubert@mozilla.com>
Sun, 21 Jul 2013 11:12:51 +0200
changeset 151612 2268ff80683a5fbdfbda4219b55af2a112c9f71b
parent 151604 9af6265e9884684838c143ca1efbf297db62855c (current diff)
parent 151611 541487a04a9c065060401d98cbbcd4e7e8042943 (diff)
child 151684 f80683d8c3e7323cca2c51186d89023d0a1b4d1b
child 151785 05ff12c00114de0b595cab891fb42336753b8462
child 170144 e4177c0221d0730145f081924a23a75731c59b98
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
first release with
nightly linux32
2268ff80683a / 25.0a1 / 20130722030226 / files
nightly linux64
2268ff80683a / 25.0a1 / 20130722030226 / files
nightly mac
2268ff80683a / 25.0a1 / 20130722030226 / files
nightly win32
2268ff80683a / 25.0a1 / 20130722030226 / files
nightly win64
2268ff80683a / 25.0a1 / 20130722030226 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge m-c to fx-team
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -595,16 +595,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 (function(module) {
   let prefSvc = "@mozilla.org/preferences-service;1";
   XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
     let prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
     return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
   });
 
+  XPCOMUtils.defineLazyGetter(this, 'supportsString', function() {
+    return Cc["@mozilla.org/supports-string;1"]
+             .createInstance(Ci.nsISupportsString);
+  });
+
   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";
 
   /**
@@ -714,44 +719,84 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           console.error("Command file '" + aFileEntry.name + "' does not have top level array.");
           return;
         }
 
         data.forEach(function(commandSpec) {
           gcli.addCommand(commandSpec);
           commands.push(commandSpec.name);
         });
-
       },
       function onError(reason) {
         console.error("OS.File.read(" + aFileEntry.path + ") failed.");
         throw reason;
       }
     );
   }
 
   /**
    * 'cmd' command
    */
   gcli.addCommand({
     name: "cmd",
-    get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
+    get hidden() {
+      return !prefBranch.prefHasUserValue(PREF_DIR);
+    },
     description: gcli.lookup("cmdDesc")
   });
 
   /**
    * '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) {
+    get hidden() {
+      return !prefBranch.prefHasUserValue(PREF_DIR);
+    },
+    exec: function(args, context) {
       let chromeWindow = context.environment.chromeDocument.defaultView;
       CmdCommands.refreshAutoCommands(chromeWindow);
+
+      let dirName = prefBranch.getComplexValue(PREF_DIR,
+                                              Ci.nsISupportsString).data.trim();
+      return gcli.lookupFormat("cmdStatus", [ commands.length, dirName ]);
+    }
+  });
+
+  /**
+   * 'cmd setdir' command
+   */
+  gcli.addCommand({
+    name: "cmd setdir",
+    description: gcli.lookup("cmdSetdirDesc"),
+    params: [
+      {
+        name: "directory",
+        description: gcli.lookup("cmdSetdirDirectoryDesc"),
+        type: {
+          name: "file",
+          filetype: "directory",
+          existing: "yes"
+        },
+        defaultValue: null
+      }
+    ],
+    returnType: "string",
+    get hidden() {
+      return true; // !prefBranch.prefHasUserValue(PREF_DIR);
+    },
+    exec: function(args, context) {
+      supportsString.data = args.directory;
+      prefBranch.setComplexValue(PREF_DIR, Ci.nsISupportsString, supportsString);
+
+      let chromeWindow = context.environment.chromeDocument.defaultView;
+      CmdCommands.refreshAutoCommands(chromeWindow);
+
+      return gcli.lookupFormat("cmdStatus", [ commands.length, args.directory ]);
     }
   });
 }(this));
 
 /* CmdConsole -------------------------------------------------------------- */
 
 (function(module) {
   XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
@@ -1488,17 +1533,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   gcli.addCommand({
     name: "tools srcdir",
     description: gcli.lookup("toolsSrcdirDesc"),
     manual: gcli.lookupFormat("toolsSrcdirManual2", [BRAND_SHORT_NAME]),
     get hidden() gcli.hiddenByChromePref(),
     params: [
       {
         name: "srcdir",
-        type: "string",
+        type: "string" /* {
+          name: "file",
+          filetype: "directory",
+          existing: "yes"
+        } */,
         description: gcli.lookup("toolsSrcdirDir")
       }
     ],
     returnType: "string",
     exec: function(args, context) {
       let clobber = OS.Path.join(args.srcdir, "CLOBBER");
       return OS.File.exists(clobber).then(function(exists) {
         if (exists) {
--- a/browser/devtools/commandline/test/Makefile.in
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -39,20 +39,24 @@ MOCHITEST_BROWSER_FILES = \
   browser_cmd_media.js \
   browser_cmd_pagemod_export.html \
   browser_cmd_pagemod_export.js \
   browser_cmd_pref.js \
   browser_cmd_restart.js \
   browser_cmd_screenshot.html \
   browser_cmd_screenshot.js \
   browser_cmd_settings.js \
+  browser_gcli_async.js \
   browser_gcli_canon.js \
   browser_gcli_cli.js \
   browser_gcli_completion.js \
+  browser_gcli_date.js \
   browser_gcli_exec.js \
+  browser_gcli_fail.js \
+  browser_gcli_file.js \
   browser_gcli_focus.js \
   browser_gcli_history.js \
   browser_gcli_incomplete.js \
   browser_gcli_inputter.js \
   browser_gcli_intro.js \
   browser_gcli_js.js \
   browser_gcli_keyboard1.js \
   browser_gcli_keyboard2.js \
--- a/browser/devtools/commandline/test/browser_gcli_async.js
+++ b/browser/devtools/commandline/test/browser_gcli_async.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
--- a/browser/devtools/commandline/test/browser_gcli_canon.js
+++ b/browser/devtools/commandline/test/browser_gcli_canon.js
@@ -212,20 +212,21 @@ exports.testAltCanon = function(options)
       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"]}}]}}',
+            '{"tss":{"name":"tss","description":"(No description)","params":[' +
+              '{"name":"str","type":"string","description":"(No description)"},' +
+              '{"name":"num","type":"number","description":"(No description)"},' +
+              '{"name":"opt","type":{"name":"selection","data":["1","2","3"]},"description":"(No description)"}'+
+            '],"isParent":false}}',
             '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);
   };
--- a/browser/devtools/commandline/test/browser_gcli_context.js
+++ b/browser/devtools/commandline/test/browser_gcli_context.js
@@ -32,36 +32,27 @@ function test() {
 }
 
 // <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, [
+  return 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',
@@ -95,17 +86,17 @@ exports.testBaseline = function(options)
           command: { name: 'tsn' },
         }
       }
     }
   ]);
 };
 
 exports.testContext = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     // Use the 'tsn' context
     {
       setup:    'context tsn',
       check: {
         input:  'context tsn',
         hints:             '',
         markup: 'VVVVVVVVVVV',
         message: '',
--- a/browser/devtools/commandline/test/browser_gcli_date.js
+++ b/browser/devtools/commandline/test/browser_gcli_date.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
@@ -75,18 +85,20 @@ exports.testIncrement = function(options
 
     // See comments in testParse
     var gap = new Date().getTime() - minusOne.getTime();
     assert.ok(gap < 60000, 'now is less than a minute away');
   });
 };
 
 exports.testInput = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
+      // See bug 892901
+      skipRemainingIf: options.isFirefox,
       setup:    'tsdate 2001-01-01 1980-01-03',
       check: {
         input:  'tsdate 2001-01-01 1980-01-03',
         hints:                              '',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID',
         message: '',
         args: {
@@ -127,18 +139,20 @@ exports.testInput = function(options) {
         type: 'string',
         error: false
       }
     }
   ]);
 };
 
 exports.testIncrDecr = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
+      // See bug 892901
+      skipRemainingIf: options.isFirefox,
       setup:    'tsdate 2001-01-01<UP>',
       check: {
         input:  'tsdate 2001-01-02',
         hints:                    ' <d2>',
         markup: 'VVVVVVVVVVVVVVVVV',
         status: 'ERROR',
         message: '',
         args: {
@@ -219,24 +233,24 @@ exports.testIncrDecr = function(options)
               assert.is(d1.getSeconds(), 0, 'd1 seconds');
               assert.is(d1.getMilliseconds(), 0, 'd1 millis');
             },
             arg: ' 2001-02-01',
             status: 'VALID',
             message: ''
           },
           d2: {
-            value: function(d1) {
-              assert.is(d1.getFullYear(), 2000, 'd1 year');
-              assert.is(d1.getMonth(), 1, 'd1 month');
-              assert.is(d1.getDate(), 28, 'd1 date');
-              assert.is(d1.getHours(), 0, 'd1 hours');
-              assert.is(d1.getMinutes(), 0, 'd1 minutes');
-              assert.is(d1.getSeconds(), 0, 'd1 seconds');
-              assert.is(d1.getMilliseconds(), 0, 'd1 millis');
+            value: function(d2) {
+              assert.is(d2.getFullYear(), 2000, 'd2 year');
+              assert.is(d2.getMonth(), 1, 'd2 month');
+              assert.is(d2.getDate(), 28, 'd2 date');
+              assert.is(d2.getHours(), 0, 'd2 hours');
+              assert.is(d2.getMinutes(), 0, 'd2 minutes');
+              assert.is(d2.getSeconds(), 0, 'd2 seconds');
+              assert.is(d2.getMilliseconds(), 0, 'd2 millis');
             },
             arg: ' "2000-02-28"',
             status: 'VALID',
             message: ''
           },
         }
       }
     }
--- a/browser/devtools/commandline/test/browser_gcli_fail.js
+++ b/browser/devtools/commandline/test/browser_gcli_fail.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
@@ -22,32 +32,23 @@ function test() {
 }
 
 // <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,
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_file.js
@@ -0,0 +1,848 @@
+/*
+ * 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-testFile.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');
+
+exports.setup = function(options) {
+  mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+};
+
+var local = false;
+
+exports.testBasic = function(options) {
+  var isPhantomjsFromFilesystem = (!options.isHttp && options.isPhantomjs);
+  return helpers.audit(options, [
+    {
+      // These tests require us to be using node directly or to be in
+      // phantomjs connected to an allowexec enabled node server or to be in
+      // firefox. In short they only don't work when in phantomjs reading
+      // from the filesystem, but they do work in Firefox
+      skipRemainingIf: isPhantomjsFromFilesystem || options.isFirefox,
+      setup:    'tsfile open /',
+      check: {
+        input:  'tsfile open /',
+        hints:               '',
+        markup: 'VVVVVVVVVVVVI',
+        cursor: 13,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' is not a file',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' is not a file'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile open /zxcv',
+      check: {
+        input:  'tsfile open /zxcv',
+        // hints:                   ' -> /etc/',
+        markup: 'VVVVVVVVVVVVIIIII',
+        cursor: 17,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile open /mach_kernel',
+      check: {
+        input:  'tsfile open /mach_kernel',
+        hints:                          '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 24,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+           }
+        }
+      }
+    },
+    {
+      setup:    'tsfile saveas /',
+      check: {
+        input:  'tsfile saveas /',
+        hints:                 '',
+        markup: 'VVVVVVVVVVVVVVI',
+        cursor: 15,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' already exists',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' already exists'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile saveas /zxcv',
+      check: {
+        input:  'tsfile saveas /zxcv',
+        // hints:                     ' -> /etc/',
+        markup: 'VVVVVVVVVVVVVVVVVVV',
+        cursor: 19,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile saveas /mach_kernel',
+      check: {
+        input:  'tsfile saveas /mach_kernel',
+        hints:                            '',
+        markup: 'VVVVVVVVVVVVVVIIIIIIIIIIII',
+        cursor: 26,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/mach_kernel\' already exists',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: undefined,
+            arg: ' /mach_kernel',
+            status: 'INCOMPLETE',
+            message: '\'/mach_kernel\' already exists'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile save /',
+      check: {
+        input:  'tsfile save /',
+        hints:               '',
+        markup: 'VVVVVVVVVVVVI',
+        cursor: 13,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' is not a file',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' is not a file'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile save /zxcv',
+      check: {
+        input:  'tsfile save /zxcv',
+        // hints:                   ' -> /etc/',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        cursor: 17,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile save /mach_kernel',
+      check: {
+        input:  'tsfile save /mach_kernel',
+        hints:                          '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 24,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile cd /',
+      check: {
+        input:  'tsfile cd /',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        cursor: 11,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: '/',
+            arg: ' /',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile cd /zxcv',
+      check: {
+        input:  'tsfile cd /zxcv',
+        // hints:                 ' -> /dev/',
+        markup: 'VVVVVVVVVVIIIII',
+        cursor: 15,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true || !local,
+      setup:    'tsfile cd /etc/passwd',
+      check: {
+        input:  'tsfile cd /etc/passwd',
+        hints:                       ' -> /etc/pam.d/',
+        markup: 'VVVVVVVVVVIIIIIIIIIII',
+        cursor: 21,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/etc/passwd\' is not a directory',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: undefined,
+            arg: ' /etc/passwd',
+            status: 'INCOMPLETE',
+            message: '\'/etc/passwd\' is not a directory'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile mkdir /',
+      check: {
+        input:  'tsfile mkdir /',
+        hints:                '',
+        markup: 'VVVVVVVVVVVVVI',
+        cursor: 14,
+        current: 'p1',
+        status: 'ERROR',
+        message: ''/' already exists',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' already exists'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile mkdir /zxcv',
+      check: {
+        input:  'tsfile mkdir /zxcv',
+        // hints:                    ' -> /dev/',
+        markup: 'VVVVVVVVVVVVVVVVVV',
+        cursor: 18,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile mkdir /mach_kernel',
+      check: {
+        input:  'tsfile mkdir /mach_kernel',
+        hints:                           '',
+        markup: 'VVVVVVVVVVVVVIIIIIIIIIIII',
+        cursor: 25,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/mach_kernel\' already exists',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: undefined,
+            arg: ' /mach_kernel',
+            status: 'INCOMPLETE',
+            message: '\'/mach_kernel\' already exists'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile rm /',
+      check: {
+        input:  'tsfile rm /',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        cursor: 11,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: '/',
+            arg: ' /',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile rm /zxcv',
+      check: {
+        input:  'tsfile rm /zxcv',
+        // hints:                 ' -> /etc/',
+        markup: 'VVVVVVVVVVIIIII',
+        cursor: 15,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile rm /mach_kernel',
+      check: {
+        input:  'tsfile rm /mach_kernel',
+        hints:                        '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 22,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    }
+  ]);
+};
+
+exports.testFirefoxBasic = function(options) {
+  return helpers.audit(options, [
+    {
+      // These tests are just like the ones above tailored for running in
+      // Firefox
+      skipRemainingIf: true,
+      // skipRemainingIf: !options.isFirefox,
+      skipIf: true,
+      setup:    'tsfile open /',
+      check: {
+        input:  'tsfile open /',
+        hints:               '',
+        markup: 'VVVVVVVVVVVVI',
+        cursor: 13,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' is not a file',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' is not a file'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile open /zxcv',
+      check: {
+        input:  'tsfile open /zxcv',
+        // hints:                   ' -> /etc/',
+        markup: 'VVVVVVVVVVVVIIIII',
+        cursor: 17,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile open /mach_kernel',
+      check: {
+        input:  'tsfile open /mach_kernel',
+        hints:                          '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 24,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile open' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+           }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile saveas /',
+      check: {
+        input:  'tsfile saveas /',
+        hints:                 '',
+        markup: 'VVVVVVVVVVVVVVI',
+        cursor: 15,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' already exists',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' already exists'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile saveas /zxcv',
+      check: {
+        input:  'tsfile saveas /zxcv',
+        // hints:                     ' -> /etc/',
+        markup: 'VVVVVVVVVVVVVVVVVVV',
+        cursor: 19,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile saveas /mach_kernel',
+      check: {
+        input:  'tsfile saveas /mach_kernel',
+        hints:                            '',
+        markup: 'VVVVVVVVVVVVVVIIIIIIIIIIII',
+        cursor: 26,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/mach_kernel\' already exists',
+        args: {
+          command: { name: 'tsfile saveas' },
+          p1: {
+            value: undefined,
+            arg: ' /mach_kernel',
+            status: 'INCOMPLETE',
+            message: '\'/mach_kernel\' already exists'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile save /',
+      check: {
+        input:  'tsfile save /',
+        hints:               '',
+        markup: 'VVVVVVVVVVVVI',
+        cursor: 13,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/\' is not a file',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' is not a file'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile save /zxcv',
+      check: {
+        input:  'tsfile save /zxcv',
+        // hints:                   ' -> /etc/',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        cursor: 17,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile save /mach_kernel',
+      check: {
+        input:  'tsfile save /mach_kernel',
+        hints:                          '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 24,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile save' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile cd /',
+      check: {
+        input:  'tsfile cd /',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        cursor: 11,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: '/',
+            arg: ' /',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile cd /zxcv',
+      check: {
+        input:  'tsfile cd /zxcv',
+        // hints:                 ' -> /dev/',
+        // markup: 'VVVVVVVVVVIIIII',
+        cursor: 15,
+        current: 'p1',
+        // status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            // status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true || !local,
+      setup:    'tsfile cd /etc/passwd',
+      check: {
+        input:  'tsfile cd /etc/passwd',
+        hints:                       ' -> /etc/pam.d/',
+        markup: 'VVVVVVVVVVIIIIIIIIIII',
+        cursor: 21,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/etc/passwd\' is not a directory',
+        args: {
+          command: { name: 'tsfile cd' },
+          p1: {
+            value: undefined,
+            arg: ' /etc/passwd',
+            status: 'INCOMPLETE',
+            message: '\'/etc/passwd\' is not a directory'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile mkdir /',
+      check: {
+        input:  'tsfile mkdir /',
+        hints:                '',
+        markup: 'VVVVVVVVVVVVVI',
+        cursor: 14,
+        current: 'p1',
+        status: 'ERROR',
+        message: ''/' already exists',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: undefined,
+            arg: ' /',
+            status: 'INCOMPLETE',
+            message: '\'/\' already exists'
+          }
+        }
+      }
+    },
+    {
+      setup:    'tsfile mkdir /zxcv',
+      check: {
+        input:  'tsfile mkdir /zxcv',
+        // hints:                    ' -> /dev/',
+        markup: 'VVVVVVVVVVVVVVVVVV',
+        cursor: 18,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: '/zxcv',
+            arg: ' /zxcv',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile mkdir /mach_kernel',
+      check: {
+        input:  'tsfile mkdir /mach_kernel',
+        hints:                           '',
+        markup: 'VVVVVVVVVVVVVIIIIIIIIIIII',
+        cursor: 25,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/mach_kernel\' already exists',
+        args: {
+          command: { name: 'tsfile mkdir' },
+          p1: {
+            value: undefined,
+            arg: ' /mach_kernel',
+            status: 'INCOMPLETE',
+            message: '\'/mach_kernel\' already exists'
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile rm /',
+      check: {
+        input:  'tsfile rm /',
+        hints:             '',
+        markup: 'VVVVVVVVVVV',
+        cursor: 11,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: '/',
+            arg: ' /',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    },
+    {
+      skipIf: true,
+      setup:    'tsfile rm /zxcv',
+      check: {
+        input:  'tsfile rm /zxcv',
+        // hints:                 ' -> /etc/',
+        markup: 'VVVVVVVVVVIIIII',
+        cursor: 15,
+        current: 'p1',
+        status: 'ERROR',
+        message: '\'/zxcv\' doesn\'t exist',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: undefined,
+            arg: ' /zxcv',
+            status: 'INCOMPLETE',
+            message: '\'/zxcv\' doesn\'t exist'
+          }
+        }
+      }
+    },
+    {
+      skipIf: !local,
+      setup:    'tsfile rm /mach_kernel',
+      check: {
+        input:  'tsfile rm /mach_kernel',
+        hints:                        '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 22,
+        current: 'p1',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsfile rm' },
+          p1: {
+            value: '/mach_kernel',
+            arg: ' /mach_kernel',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      }
+    }
+  ]);
+};
+
+
+// });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_fileparser.js
@@ -0,0 +1,57 @@
+/*
+ * 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-testFileparser.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+var fileparser = require('util/fileparser');
+
+var local = false;
+
+exports.testGetPredictor = function(options) {
+  if (!options.isNode || !local) {
+    return;
+  }
+
+  var options = { filetype: 'file', existing: 'yes' };
+  var predictor = fileparser.getPredictor('/usr/locl/bin/nmp', options);
+  return predictor().then(function(replies) {
+    assert.is(replies[0].name,
+              '/usr/local/bin/npm',
+              'predict npm');
+  });
+};
+
+// });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_filesystem.js
@@ -0,0 +1,78 @@
+/*
+ * 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-testFilesystem.js</p>";
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URI, function(options) {
+    return helpers.runTests(options, exports);
+  }).then(finish);
+}
+
+// <INJECTED SOURCE:END>
+
+'use strict';
+
+// var assert = require('test/assert');
+// var helpers = require('gclitest/helpers');
+var filesystem = require('util/filesystem');
+
+exports.testSplit = function(options) {
+  if (!options.isNode) {
+    return;
+  }
+
+  helpers.arrayIs(filesystem.split('', '/'),
+                  [ '.' ],
+                  'split <blank>');
+
+  helpers.arrayIs(filesystem.split('a', '/'),
+                  [ 'a' ],
+                  'split a');
+
+  helpers.arrayIs(filesystem.split('a/b/c', '/'),
+                  [ 'a', 'b', 'c' ],
+                  'split a/b/c');
+
+  helpers.arrayIs(filesystem.split('/a/b/c/', '/'),
+                  [ 'a', 'b', 'c' ],
+                  'split a/b/c');
+
+  helpers.arrayIs(filesystem.split('/a/b///c/', '/'),
+                  [ 'a', 'b', 'c' ],
+                  'split a/b/c');
+};
+
+exports.testJoin = function(options) {
+  if (!options.isNode) {
+    return;
+  }
+
+  assert.is(filesystem.join('usr', 'local', 'bin'),
+            'usr/local/bin',
+            'join to usr/local/bin');
+};
+
+// });
--- a/browser/devtools/commandline/test/browser_gcli_focus.js
+++ b/browser/devtools/commandline/test/browser_gcli_focus.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
--- a/browser/devtools/commandline/test/browser_gcli_incomplete.js
+++ b/browser/devtools/commandline/test/browser_gcli_incomplete.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
--- a/browser/devtools/commandline/test/browser_gcli_menu.js
+++ b/browser/devtools/commandline/test/browser_gcli_menu.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
--- a/browser/devtools/commandline/test/browser_gcli_node.js
+++ b/browser/devtools/commandline/test/browser_gcli_node.js
@@ -1,12 +1,22 @@
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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
--- a/browser/devtools/commandline/test/browser_gcli_remote.js
+++ b/browser/devtools/commandline/test/browser_gcli_remote.js
@@ -43,16 +43,17 @@ exports.setup = function(options) {
   mockCommands.setup();
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
 };
 
 exports.testRemote = function(options) {
+  var connected = false;
   return helpers.audit(options, [
     {
       skipRemainingIf: !options.isHttp,
       setup:    'remote ',
       check: {
         input:  'remote ',
         hints:         '',
         markup: 'EEEEEEV',
@@ -81,16 +82,82 @@ exports.testRemote = function(options) {
         args: {
           command: { name: 'connect' },
           prefix: { value: 'remote', arg: ' remote', status: 'VALID', message: '' },
           host: { value: undefined, arg: '', status: 'VALID', message: '' },
           port: { value: undefined, arg: '', status: 'VALID', message: '' },
         }
       },
       exec: {
+        completed: false,
+        error: false
+      },
+      post: function(output, data) {
+        connected = !output.error;
+        if (!connected) {
+          console.log('Failure from "connect remote". Run server with "node gcli server start --websocket --allowexec" to allow remote command testing');
+        }
+      }
+    },
+    {
+      // We do a connect-disconnect dance for 2 reasons, partly re-establishing
+      // a connection is a good test, and secondly it lets us have minimal
+      // testing on the first connection so we don't need to turn websockets
+      // on all the time
+      setup:    'disconnect remote --force',
+      skipRemainingIf: !connected,
+      check: {
+        input:  'disconnect remote --force',
+        hints:                           '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV',
+        cursor: 25,
+        current: 'force',
+        status: 'VALID',
+        message: '',
+        unassigned: [ ],
+        args: {
+          command: { name: 'disconnect' },
+          prefix: {
+            value: function(connection) {
+              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            },
+            arg: ' remote',
+            status: 'VALID',
+            message: ''
+          }
+        }
+      },
+      exec: {
+        output: /^Removed [0-9]* commands.$/,
+        completed: true,
+        type: 'string',
+        error: false
+      }
+    },
+    {
+      setup:    'connect remote',
+      check: {
+        input:  'connect remote',
+        hints:                ' [options]',
+        markup: 'VVVVVVVVVVVVVV',
+        cursor: 14,
+        current: 'prefix',
+        status: 'VALID',
+        options: [ ],
+        message: '',
+        predictions: [ ],
+        unassigned: [ ],
+        args: {
+          command: { name: 'connect' },
+          prefix: { value: 'remote', arg: ' remote', status: 'VALID', message: '' },
+          host: { value: undefined, arg: '', status: 'VALID', message: '' },
+          port: { value: undefined, arg: '', status: 'VALID', message: '' },
+        }
+      },
+      exec: {
         output: /^Added [0-9]* commands.$/,
         completed: false,
         type: 'string',
         error: false
       }
     },
     {
       setup:    'remote ',
--- a/browser/devtools/commandline/test/browser_gcli_spell.js
+++ b/browser/devtools/commandline/test/browser_gcli_spell.js
@@ -30,17 +30,17 @@ function test() {
   }).then(finish);
 }
 
 // <INJECTED SOURCE:END>
 
 'use strict';
 
 // var assert = require('test/assert');
-var spell = require('gcli/types/spell');
+var spell = require('util/spell');
 
 exports.testSpellerSimple = function(options) {
   var alternatives = Object.keys(options.window);
 
   assert.is(spell.correct('document', alternatives), 'document');
   assert.is(spell.correct('documen', alternatives), 'document');
 
   if (options.isJsdom) {
@@ -49,10 +49,39 @@ exports.testSpellerSimple = function(opt
   else {
     assert.is(spell.correct('ocument', alternatives), 'document');
   }
   assert.is(spell.correct('odcument', alternatives), 'document');
 
   assert.is(spell.correct('=========', alternatives), undefined);
 };
 
+exports.testRank = function(options) {
+  var distances = spell.rank('fred', [ 'banana', 'fred', 'ed', 'red', 'FRED' ]);
+
+  assert.is(distances.length, 5, 'rank length');
+
+  assert.is(distances[0].name, 'fred', 'fred name #0');
+  assert.is(distances[1].name, 'FRED', 'FRED name #1');
+  assert.is(distances[2].name, 'red', 'red name #2');
+  assert.is(distances[3].name, 'ed', 'ed name #3');
+  assert.is(distances[4].name, 'banana', 'banana name #4');
+
+  assert.is(distances[0].dist, 0, 'fred dist 0');
+  assert.is(distances[1].dist, 4, 'FRED dist 4');
+  assert.is(distances[2].dist, 10, 'red dist 10');
+  assert.is(distances[3].dist, 20, 'ed dist 20');
+  assert.is(distances[4].dist, 100, 'banana dist 100');
+};
+
+exports.testRank2 = function(options) {
+  var distances = spell.rank('caps', [ 'CAPS', 'false' ]);
+  assert.is(JSON.stringify(distances),
+            '[{"name":"CAPS","dist":4},{"name":"false","dist":50}]',
+            'spell.rank("caps", [ "CAPS", "false" ]');
+};
+
+exports.testDistancePrefix = function(options) {
+  assert.is(spell.distancePrefix('fred', 'freddy'), 0, 'distancePrefix fred');
+  assert.is(spell.distancePrefix('FRED', 'freddy'), 4, 'distancePrefix FRED');
+};
 
 // });
--- a/browser/devtools/commandline/test/browser_gcli_string.js
+++ b/browser/devtools/commandline/test/browser_gcli_string.js
@@ -42,17 +42,17 @@ exports.setup = function(options) {
   mockCommands.setup();
 };
 
 exports.shutdown = function(options) {
   mockCommands.shutdown();
 };
 
 exports.testNewLine = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
       setup:    'echo a\\nb',
       check: {
         input:  'echo a\\nb',
         hints:            '',
         markup: 'VVVVVVVVV',
         cursor: 9,
         current: 'message',
@@ -67,17 +67,17 @@ exports.testNewLine = function(options) 
           }
         }
       }
     }
   ]);
 };
 
 exports.testTab = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
       setup:    'echo a\\tb',
       check: {
         input:  'echo a\\tb',
         hints:            '',
         markup: 'VVVVVVVVV',
         cursor: 9,
         current: 'message',
@@ -92,17 +92,17 @@ exports.testTab = function(options) {
           }
         }
       }
     }
   ]);
 };
 
 exports.testEscape = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
       // What's typed is actually:
       //         tsrsrsr a\\ b c
       setup:    'tsrsrsr a\\\\ b c',
       check: {
         input:  'tsrsrsr a\\\\ b c',
         hints:                 '',
         markup: 'VVVVVVVVVVVVVVV',
@@ -138,17 +138,17 @@ exports.testEscape = function(options) {
           p3: { value: 'asd', arg: ' asd', status: 'VALID', message: '' },
         }
       }
     }
   ]);
 };
 
 exports.testBlank = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
       setup:    'tsrsrsr a "" c',
       check: {
         input:  'tsrsrsr a "" c',
         hints:                '',
         markup: 'VVVVVVVVVVVVVV',
         cursor: 14,
         current: 'p3',
@@ -208,17 +208,17 @@ exports.testBlank = function(options) {
           }
         }
       }
     }
   ]);
 };
 
 exports.testBlankWithParam = function(options) {
-  helpers.audit(options, [
+  return helpers.audit(options, [
     {
       setup:    'tsrsrsr  a --p3',
       check: {
         input:  'tsrsrsr  a --p3',
         hints:                 ' <string> <p2>',
         markup: 'VVVVVVVVVVVVVVV',
         cursor: 15,
         current: 'p3',
--- a/browser/devtools/commandline/test/helpers.js
+++ b/browser/devtools/commandline/test/helpers.js
@@ -25,16 +25,17 @@ Components.utils.import("resource://gre/
 
 let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
 let TargetFactory = (Cu.import("resource://gre/modules/devtools/Loader.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 cli = require('gcli/cli');
 
 var converters = require('gcli/converters');
 
 /**
  * Warning: For use with Firefox Mochitests only.
  *
  * Open a new tab at a URL and call a callback on load, and then tidy up when
  * the callback finishes.
@@ -561,17 +562,17 @@ helpers._check = function(options, name,
     var hintCheck = function(actualHints) {
       assert.is(actualHints, checks.hints, 'hints' + suffix);
     };
     outstanding.push(helpers._actual.hints(options).then(hintCheck));
   }
 
   if ('predictions' in checks) {
     var predictionsCheck = function(actualPredictions) {
-      helpers._arrayIs(actualPredictions,
+      helpers.arrayIs(actualPredictions,
                        checks.predictions,
                        'predictions' + suffix);
     };
     outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
   }
 
   if ('predictionsContains' in checks) {
     var containsCheck = function(actualPredictions) {
@@ -580,17 +581,17 @@ helpers._check = function(options, name,
         assert.ok(index !== -1,
                   'predictionsContains:' + prediction + suffix);
       });
     };
     outstanding.push(helpers._actual.predictions(options).then(containsCheck));
   }
 
   if ('unassigned' in checks) {
-    helpers._arrayIs(helpers._actual.unassigned(options),
+    helpers.arrayIs(helpers._actual.unassigned(options),
                      checks.unassigned,
                      'unassigned' + suffix);
   }
 
   if ('tooltipState' in checks) {
     if (options.isJsdom) {
       assert.log('Skipped ' + name + '/tooltipState due to jsdom');
     }
@@ -608,17 +609,17 @@ helpers._check = function(options, name,
     else {
       assert.is(helpers._actual.outputState(options),
                 checks.outputState,
                 'outputState' + suffix);
     }
   }
 
   if ('options' in checks) {
-    helpers._arrayIs(helpers._actual.options(options),
+    helpers.arrayIs(helpers._actual.options(options),
                      checks.options,
                      'options' + suffix);
   }
 
   if ('error' in checks) {
     assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
   }
 
@@ -713,39 +714,54 @@ helpers._check = function(options, name,
  * @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({});
   }
 
+  var origLogErrors = cli.logErrors;
+  if (expected.error) {
+    cli.logErrors = false;
+  }
+
   var output;
   try {
     output = options.display.requisition.exec({ hidden: true });
   }
   catch (ex) {
     assert.ok(false, 'Failure executing \'' + name + '\': ' + ex);
     util.errorHandler(ex);
 
+    if (expected.error) {
+      cli.logErrors = origLogErrors;
+    }
     return promise.resolve({});
   }
 
   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);
+
+    if (expected.error) {
+      cli.logErrors = origLogErrors;
+    }
     return promise.resolve({ output: output });
   }
 
   if (!('output' in expected)) {
+    if (expected.error) {
+      cli.logErrors = origLogErrors;
+    }
     return promise.resolve({ output: output });
   }
 
   var checkOutput = function() {
     if ('type' in expected) {
       assert.is(output.type,
                 expected.type,
                 'output.type for: ' + name);
@@ -783,16 +799,19 @@ helpers._exec = function(options, name, 
         expected.output.forEach(function(match) {
           doTest(match, actualOutput);
         });
       }
       else {
         doTest(expected.output, actualOutput);
       }
 
+      if (expected.error) {
+        cli.logErrors = origLogErrors;
+      }
       return { output: output, text: actualOutput };
     });
   };
 
   return output.promise.then(checkOutput, checkOutput);
 };
 
 /**
@@ -935,40 +954,40 @@ helpers.audit = function(options, audits
     if (name == null && typeof audit.setup === 'string') {
       name = audit.setup;
     }
 
     if (assert.testLogging) {
       log('- START \'' + name + '\' in ' + assert.currentTest);
     }
 
-    if (audit.skipIf) {
-      var skip = (typeof audit.skipIf === 'function') ?
-          audit.skipIf(options) :
-          !!audit.skipIf;
-      if (skip) {
-        var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : '';
-        assert.log('Skipped ' + name + ' ' + reason);
-        return promise.resolve(undefined);
-      }
-    }
-
     if (audit.skipRemainingIf) {
       var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ?
           audit.skipRemainingIf(options) :
           !!audit.skipRemainingIf;
       if (skipRemainingIf) {
         skipReason = audit.skipRemainingIf.name ?
             'due to ' + audit.skipRemainingIf.name :
             '';
         assert.log('Skipped ' + name + ' ' + skipReason);
         return promise.resolve(undefined);
       }
     }
 
+    if (audit.skipIf) {
+      var skip = (typeof audit.skipIf === 'function') ?
+          audit.skipIf(options) :
+          !!audit.skipIf;
+      if (skip) {
+        var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : '';
+        assert.log('Skipped ' + name + ' ' + reason);
+        return promise.resolve(undefined);
+      }
+    }
+
     if (skipReason != null) {
       assert.log('Skipped ' + name + ' ' + skipReason);
       return promise.resolve(undefined);
     }
 
     var start = new Date().getTime();
 
     var setupDone = helpers._setup(options, name, audit.setup);
@@ -1003,17 +1022,17 @@ helpers.audit = function(options, audits
   }).then(function() {
     return options.display.inputter.setInput('');
   });
 };
 
 /**
  * Compare 2 arrays.
  */
-helpers._arrayIs = function(actual, expected, message) {
+helpers.arrayIs = function(actual, expected, message) {
   assert.ok(Array.isArray(actual), 'actual is not an array: ' + message);
   assert.ok(Array.isArray(expected), 'expected is not an array: ' + message);
 
   if (!Array.isArray(actual) || !Array.isArray(expected)) {
     return;
   }
 
   assert.is(actual.length, expected.length, 'array length: ' + message);
--- a/browser/devtools/commandline/test/mockCommands.js
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -477,16 +477,126 @@ var tsfail = {
     if (args.method === 'throwstring') {
       throw 'thrown string';
     }
 
     return 'no error';
   }
 };
 
+var tsfile = {
+  item: 'command',
+  name: 'tsfile',
+  description: 'test file params',
+};
+
+var tsfileOpen = {
+  item: 'command',
+  name: 'tsfile open',
+  description: 'a file param in open mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'file',
+        existing: 'yes'
+      }
+    }
+  ],
+  exec: createExec('tsfile open')
+};
+
+var tsfileSaveas = {
+  item: 'command',
+  name: 'tsfile saveas',
+  description: 'a file param in saveas mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'file',
+        existing: 'no'
+      }
+    }
+  ],
+  exec: createExec('tsfile saveas')
+};
+
+var tsfileSave = {
+  item: 'command',
+  name: 'tsfile save',
+  description: 'a file param in save mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'file',
+        existing: 'maybe'
+      }
+    }
+  ],
+  exec: createExec('tsfile save')
+};
+
+var tsfileCd = {
+  item: 'command',
+  name: 'tsfile cd',
+  description: 'a file param in cd mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'directory',
+        existing: 'yes'
+      }
+    }
+  ],
+  exec: createExec('tsfile cd')
+};
+
+var tsfileMkdir = {
+  item: 'command',
+  name: 'tsfile mkdir',
+  description: 'a file param in mkdir mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'directory',
+        existing: 'no'
+      }
+    }
+  ],
+  exec: createExec('tsfile mkdir')
+};
+
+var tsfileRm = {
+  item: 'command',
+  name: 'tsfile rm',
+  description: 'a file param in rm mode',
+  params: [
+    {
+      name: 'p1',
+      type: {
+        name: 'file',
+        filetype: 'any',
+        existing: 'yes'
+      }
+    }
+  ],
+  exec: createExec('tsfile rm')
+};
+
+
+
 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
@@ -528,16 +638,23 @@ mockCommands.setup = function(opts) {
   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.tsdate = canon.addCommand(tsdate);
   mockCommands.commands.tsfail = canon.addCommand(tsfail);
+  mockCommands.commands.tsfile = canon.addCommand(tsfile);
+  mockCommands.commands.tsfileOpen = canon.addCommand(tsfileOpen);
+  mockCommands.commands.tsfileSaveas = canon.addCommand(tsfileSaveas);
+  mockCommands.commands.tsfileSave = canon.addCommand(tsfileSave);
+  mockCommands.commands.tsfileCd = canon.addCommand(tsfileCd);
+  mockCommands.commands.tsfileMkdir = canon.addCommand(tsfileMkdir);
+  mockCommands.commands.tsfileRm = canon.addCommand(tsfileRm);
 };
 
 mockCommands.shutdown = function(opts) {
   canon.removeCommand(tsv);
   canon.removeCommand(tsr);
   canon.removeCommand(tsrsrsr);
   canon.removeCommand(tso);
   canon.removeCommand(tse);
@@ -559,16 +676,23 @@ mockCommands.shutdown = function(opts) {
   canon.removeCommand(tselarr);
   canon.removeCommand(tsm);
   canon.removeCommand(tsg);
   canon.removeCommand(tshidden);
   canon.removeCommand(tscook);
   canon.removeCommand(tslong);
   canon.removeCommand(tsdate);
   canon.removeCommand(tsfail);
+  canon.removeCommand(tsfile);
+  canon.removeCommand(tsfileOpen);
+  canon.removeCommand(tsfileSaveas);
+  canon.removeCommand(tsfileSave);
+  canon.removeCommand(tsfileCd);
+  canon.removeCommand(tsfileMkdir);
+  canon.removeCommand(tsfileRm);
 
   types.removeType(mockCommands.optionType);
   types.removeType(mockCommands.optionValue);
 
   mockCommands.commands = {};
 };
 
 
--- a/browser/devtools/framework/test/Makefile.in
+++ b/browser/devtools/framework/test/Makefile.in
@@ -17,16 +17,17 @@ MOCHITEST_BROWSER_FILES = \
 		browser_toolbox_dynamic_registration.js \
 		browser_toolbox_hosts.js \
 		browser_toolbox_ready.js \
 		browser_toolbox_select_event.js \
 		browser_target_events.js \
 		browser_toolbox_tool_ready.js \
 		browser_toolbox_sidebar.js \
 		browser_toolbox_window_shortcuts.js \
+		browser_toolbox_tabsswitch_shortcuts.js \
 		browser_toolbox_window_title_changes.js \
 		browser_toolbox_options.js \
 		browser_toolbox_options_disablejs.js \
 		browser_toolbox_options_disablejs.html \
 		browser_toolbox_options_disablejs_iframe.html \
 		browser_toolbox_highlight.js \
 		browser_toolbox_raise.js \
 		$(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Toolbox = devtools.Toolbox;
+
+let toolbox, toolIDs, idIndex, secondTime = false,
+    reverse = false, nextKey = null, prevKey = null;
+
+function test() {
+  waitForExplicitFinish();
+
+  addTab("about:blank", function() {
+    toolIDs = [tool.id for (tool of gDevTools.getToolDefinitionArray())];
+    let target = TargetFactory.forTab(gBrowser.selectedTab);
+    idIndex = 0;
+    gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM)
+             .then(testShortcuts);
+  });
+}
+
+function testShortcuts(aToolbox, aIndex) {
+  if (aIndex == toolIDs.length) {
+    aIndex = 0;
+    if (secondTime) {
+      secondTime = false;
+      reverse = true;
+      aIndex = toolIDs.length - 2;
+    }
+    else {
+      secondTime = true;
+    }
+  }
+  else if (aIndex == -1) {
+    aIndex = toolIDs.length - 1;
+    if (secondTime) {
+      tidyUp();
+      return;
+    }
+    secondTime = true;
+  }
+
+  toolbox = aToolbox;
+  if (!nextKey) {
+    nextKey = toolbox.doc.getElementById("toolbox-next-tool-key")
+                     .getAttribute("key");
+    prevKey = toolbox.doc.getElementById("toolbox-previous-tool-key")
+                     .getAttribute("key");
+  }
+  info("Toolbox fired a `ready` event");
+
+  toolbox.once("select", onSelect);
+
+  if (aIndex != null) {
+    // This if block is to allow the call of onSelect without shortcut press for
+    // the first time. That happens because on opening of toolbox, one tool gets
+    // selected atleast.
+
+    let key = (reverse ? prevKey: nextKey);
+    let modifiers = {
+      accelKey: true
+    };
+    idIndex = aIndex;
+    info("Testing shortcut to switch to tool " + aIndex + ":" + toolIDs[aIndex] +
+         " using key " + key);
+    EventUtils.synthesizeKey(key, modifiers, toolbox.doc.defaultView);
+  }
+}
+
+function onSelect(event, id) {
+  info("toolbox-select event from " + id);
+
+  is(toolIDs.indexOf(id), idIndex,
+     "Correct tool is selected on pressing the shortcut for " + id);
+  // Execute soon to reset the stack trace.
+  executeSoon(() => {
+    testShortcuts(toolbox, idIndex + (reverse ? -1: 1));
+  });
+}
+
+function tidyUp() {
+  toolbox.destroy().then(function() {
+    gBrowser.removeCurrentTab();
+
+    toolbox = toolIDs = idIndex = Toolbox = secondTime = reverse = nextKey =
+      prevKey = null;
+    finish();
+  });
+}
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -202,16 +202,17 @@ Toolbox.prototype = {
         let closeButton = this.doc.getElementById("toolbox-close");
         closeButton.addEventListener("command", this.destroy, true);
 
         this._buildDockButtons();
         this._buildOptions();
         this._buildTabs();
         this._buildButtons();
         this._addKeysToWindow();
+        this._addToolSwitchingKeys();
 
         this._telemetry.toolOpened("toolbox");
 
         this.selectTool(this._defaultToolId).then(function(panel) {
           this.emit("ready");
           deferred.resolve();
         }.bind(this));
       };
@@ -225,16 +226,23 @@ Toolbox.prototype = {
 
   _buildOptions: function TBOX__buildOptions() {
     let key = this.doc.getElementById("toolbox-options-key");
     key.addEventListener("command", function(toolId) {
       this.selectTool(toolId);
     }.bind(this, "options"), true);
   },
 
+  _addToolSwitchingKeys: function TBOX__addToolSwitchingKeys() {
+    let nextKey = this.doc.getElementById("toolbox-next-tool-key");
+    nextKey.addEventListener("command", this.selectNextTool.bind(this), true);
+    let prevKey = this.doc.getElementById("toolbox-previous-tool-key");
+    prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true);
+  },
+
   /**
    * Adds the keys and commands to the Toolbox Window in window mode.
    */
   _addKeysToWindow: function TBOX__addKeysToWindow() {
     if (this.hostType != Toolbox.HostType.WINDOW) {
       return;
     }
     let doc = this.doc.defaultView.parent.document;
@@ -535,16 +543,36 @@ Toolbox.prototype = {
     return this.loadTool(id).then((panel) => {
       this.emit("select", id);
       this.emit(id + "-selected", panel);
       return panel;
     });
   },
 
   /**
+   * Loads the tool next to the currently selected tool.
+   */
+  selectNextTool: function TBOX_selectNextTool() {
+    let selected = this.doc.querySelector(".devtools-tab[selected]");
+    let next = selected.nextSibling || selected.parentNode.firstChild;
+    let tool = next.getAttribute("toolid");
+    return this.selectTool(tool);
+  },
+
+  /**
+   * Loads the tool just left to the currently selected tool.
+   */
+  selectPreviousTool: function TBOX_selectPreviousTool() {
+    let selected = this.doc.querySelector(".devtools-tab[selected]");
+    let previous = selected.previousSibling || selected.parentNode.lastChild;
+    let tool = previous.getAttribute("toolid");
+    return this.selectTool(tool);
+  },
+
+  /**
    * Highlights the tool's tab if it is not the currently selected tool.
    *
    * @param {string} id
    *        The id of the tool to highlight
    */
   highlightTool: function TBOX_highlightTool(id) {
     let tab = this.doc.getElementById("toolbox-tab-" + id);
     tab && tab.classList.add("highlighted");
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -18,16 +18,24 @@
 
   <commandset id="editMenuCommands"/>
   <keyset id="editMenuKeys"/>
   <keyset id="toolbox-keyset">
     <key id="toolbox-options-key"
          key="&toolboxOptionsButton.key;"
          oncommand="void(0);"
          modifiers="shift, accel"/>
+    <key id="toolbox-next-tool-key"
+         key="&toolboxNextTool.key;"
+         oncommand="void(0);"
+         modifiers="accel"/>
+    <key id="toolbox-previous-tool-key"
+         key="&toolboxPreviousTool.key;"
+         oncommand="void(0);"
+         modifiers="accel"/>
   </keyset>
 
   <notificationbox id="toolbox-notificationbox" flex="1">
     <toolbar class="devtools-tabbar">
 #ifdef XP_MACOSX
       <hbox id="toolbox-controls">
         <toolbarbutton id="toolbox-close"
                        class="devtools-closebutton"
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -219,28 +219,29 @@ InspectorPanel.prototype = {
    */
   setupSearchBox: function InspectorPanel_setupSearchBox() {
     let searchDoc;
     if (this.target.isLocalTab) {
       searchDoc = this.browser.contentDocument;
     } else if (this.target.window) {
       searchDoc = this.target.window.document;
     } else {
-      return;
+      searchDoc = null;
     }
     // Initiate the selectors search object.
-    let setNodeFunction = function(node) {
-      this.selection.setNode(node, "selectorsearch");
+    let setNodeFunction = function(eventName, node) {
+      this.selection.setNodeFront(node, "selectorsearch");
     }.bind(this);
     if (this.searchSuggestions) {
       this.searchSuggestions.destroy();
       this.searchSuggestions = null;
     }
     this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchSuggestions = new SelectorSearch(searchDoc, this.searchBox, setNodeFunction);
+    this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox);
+    this.searchSuggestions.on("node-selected", setNodeFunction);
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function InspectorPanel_setupSidebar() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector");
@@ -630,34 +631,38 @@ InspectorPanel.prototype = {
   /**
    * Copy the innerHTML of the selected Node to the clipboard.
    */
   copyInnerHTML: function InspectorPanel_copyInnerHTML()
   {
     if (!this.selection.isNode()) {
       return;
     }
-    let toCopy = this.selection.node.innerHTML;
-    if (toCopy) {
-      clipboardHelper.copyString(toCopy);
-    }
+    this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
   },
 
   /**
    * Copy the outerHTML of the selected Node to the clipboard.
    */
   copyOuterHTML: function InspectorPanel_copyOuterHTML()
   {
     if (!this.selection.isNode()) {
       return;
     }
-    let toCopy = this.selection.node.outerHTML;
-    if (toCopy) {
-      clipboardHelper.copyString(toCopy);
-    }
+
+    this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront));
+  },
+
+  _copyLongStr: function(promise) {
+    return promise.then(longstr => {
+      return longstr.string().then(toCopy => {
+        longstr.release().then(null, console.error);
+        clipboardHelper.copyString(toCopy);
+      });
+    }).then(null, console.error);
   },
 
   /**
    * Copy a unique selector of the selected Node to the clipboard.
    */
   copyUniqueSelector: function InspectorPanel_copyUniqueSelector()
   {
     if (!this.selection.isNode()) {
@@ -680,18 +685,17 @@ InspectorPanel.prototype = {
     }
 
     // If the markup panel is active, use the markup panel to delete
     // the node, making this an undoable action.
     if (this.markup) {
       this.markup.deleteNode(this.selection.nodeFront);
     } else {
       // remove the node from content
-      let parent = this.selection.node.parentNode;
-      parent.removeChild(this.selection.node);
+      this.walker.removeNode(this.selection.nodeFront);
     }
   },
 
   /**
    * Schedule a low-priority change event for things like paint
    * and resize.
    */
   scheduleLayoutChange: function Inspector_scheduleLayoutChange()
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,39 +1,42 @@
 /* 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";
 
 const {Cu} = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("sdk/core/promise");
 
 loader.lazyGetter(this, "AutocompletePopup", () => {
   return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
 });
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
  *
  * @constructor
+ * @param InspectorPanel aInspector
+ *        The InspectorPanel whose `walker` attribute should be used for
+ *        document traversal.
  * @param nsIDOMDocument aContentDocument
- *        The content document which inspector is attached to.
+ *        The content document which inspector is attached to, or null if
+ *        a remote document.
  * @param nsiInputElement aInputNode
  *        The input element to which the panel will be attached and from where
  *        search input will be taken.
- * @param Function aCallback
- *        The method to callback when a search is available.
- *        This method is called with the matched node as the first argument.
  */
-function SelectorSearch(aContentDocument, aInputNode, aCallback) {
+function SelectorSearch(aInspector, aContentDocument, aInputNode) {
+  this.inspector = aInspector;
   this.doc = aContentDocument;
-  this.callback = aCallback;
   this.searchBox = aInputNode;
   this.panelDoc = this.searchBox.ownerDocument;
 
   // initialize variables.
   this._lastSearched = null;
   this._lastValidSearch = "";
   this._lastToLastValidSearch = null;
   this._searchResults = null;
@@ -57,22 +60,30 @@ function SelectorSearch(aContentDocument
     onClick: this._onListBoxKeypress,
     onKeypress: this._onListBoxKeypress,
   };
   this.searchPopup = new AutocompletePopup(this.panelDoc, options);
 
   // event listeners.
   this.searchBox.addEventListener("command", this._onHTMLSearch, true);
   this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
+
+  // For testing, we need to be able to wait for the most recent node request
+  // to finish.  Tests can watch this promise for that.
+  this._lastQuery = promise.resolve(null);
+
+  EventEmitter.decorate(this);
 }
 
 exports.SelectorSearch = SelectorSearch;
 
 SelectorSearch.prototype = {
 
+  get walker() this.inspector.walker,
+
   // The possible states of the query.
   States: {
     CLASS: "class",
     ID: "id",
     TAG: "tag",
   },
 
   // The current state of the query.
@@ -163,101 +174,122 @@ SelectorSearch.prototype = {
     this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
     this.searchPopup.destroy();
     this.searchPopup = null;
     this.searchBox = null;
     this.doc = null;
     this.panelDoc = null;
     this._searchResults = null;
     this._searchSuggestions = null;
-    this.callback = null;
+    EventEmitter.decorate(this);
+  },
+
+  _selectResult: function(index) {
+    return this._searchResults.item(index).then(node => {
+      this.emit("node-selected", node);
+    });
   },
 
   /**
    * The command callback for the input box. This function is automatically
    * invoked as the user is typing if the input box type is search.
    */
   _onHTMLSearch: function SelectorSearch__onHTMLSearch() {
     let query = this.searchBox.value;
     if (query == this._lastSearched) {
       return;
     }
     this._lastSearched = query;
+    this._searchResults = null;
     this._searchIndex = 0;
 
     if (query.length == 0) {
       this._lastValidSearch = "";
       this.searchBox.removeAttribute("filled");
       this.searchBox.classList.remove("devtools-no-search-result");
       if (this.searchPopup.isOpen) {
         this.searchPopup.hidePopup();
       }
       return;
     }
 
     this.searchBox.setAttribute("filled", true);
-    try {
-      this._searchResults = this.doc.querySelectorAll(query);
-    }
-    catch (ex) {
-      this._searchResults = [];
-    }
-    if (this._searchResults.length > 0) {
-      this._lastValidSearch = query;
-      // Even though the selector matched atleast one node, there is still
-      // possibility of suggestions.
-      if (query.match(/[\s>+]$/)) {
-        // If the query has a space or '>' at the end, create a selector to match
-        // the children of the selector inside the search box by adding a '*'.
-        this._lastValidSearch += "*";
-      }
-      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
-        // If the query is a partial descendant selector which does not matches
-        // any node, remove the last incomplete part and add a '*' to match
-        // everything. For ex, convert 'foo > b' to 'foo > *' .
-        let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
-        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+    let queryList = null;
+
+    this._lastQuery = this.walker.querySelectorAll(this.walker.rootNode, query).then(list => {
+      return list;
+    }, (err) => {
+      // Failures are ok here, just use a null item list;
+      return null;
+    }).then(queryList => {
+      // Value has changed since we started this request, we're done.
+      if (query != this.searchBox.value) {
+        if (queryList) {
+          queryList.release();
+        }
+        return promise.reject(null);
       }
 
-      if (!query.slice(-1).match(/[\.#\s>+]/)) {
-        // Hide the popup if we have some matching nodes and the query is not
-        // ending with [.# >] which means that the selector is not at the
-        // beginning of a new class, tag or id.
-        if (this.searchPopup.isOpen) {
-          this.searchPopup.hidePopup();
+      this._searchResults = queryList;
+      if (this._searchResults && this._searchResults.length > 0) {
+        this._lastValidSearch = query;
+        // Even though the selector matched atleast one node, there is still
+        // possibility of suggestions.
+        if (query.match(/[\s>+]$/)) {
+          // If the query has a space or '>' at the end, create a selector to match
+          // the children of the selector inside the search box by adding a '*'.
+          this._lastValidSearch += "*";
         }
+        else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+          // If the query is a partial descendant selector which does not matches
+          // any node, remove the last incomplete part and add a '*' to match
+          // everything. For ex, convert 'foo > b' to 'foo > *' .
+          let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
+          this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+        }
+
+        if (!query.slice(-1).match(/[\.#\s>+]/)) {
+          // Hide the popup if we have some matching nodes and the query is not
+          // ending with [.# >] which means that the selector is not at the
+          // beginning of a new class, tag or id.
+          if (this.searchPopup.isOpen) {
+            this.searchPopup.hidePopup();
+          }
+        }
+        else {
+          this.showSuggestions();
+        }
+        this.searchBox.classList.remove("devtools-no-search-result");
+
+        return this._selectResult(0);
       }
       else {
+        if (query.match(/[\s>+]$/)) {
+          this._lastValidSearch = query + "*";
+        }
+        else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+          let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
+          this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+        }
+        this.searchBox.classList.add("devtools-no-search-result");
         this.showSuggestions();
       }
-      this.searchBox.classList.remove("devtools-no-search-result");
-      this.callback(this._searchResults[0]);
-    }
-    else {
-      if (query.match(/[\s>+]$/)) {
-        this._lastValidSearch = query + "*";
-      }
-      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
-        let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
-        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
-      }
-      this.searchBox.classList.add("devtools-no-search-result");
-      this.showSuggestions();
-    }
+      return undefined;
+    });
   },
 
   /**
    * Handles keypresses inside the input box.
    */
   _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
     let query = this.searchBox.value;
     switch(aEvent.keyCode) {
       case aEvent.DOM_VK_ENTER:
       case aEvent.DOM_VK_RETURN:
-        if (query == this._lastSearched) {
+        if (query == this._lastSearched && this._searchResults) {
           this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         }
         else {
           this._onHTMLSearch();
           return;
         }
         break;
 
@@ -310,18 +342,18 @@ SelectorSearch.prototype = {
         return;
 
       default:
         return;
     }
 
     aEvent.preventDefault();
     aEvent.stopPropagation();
-    if (this._searchResults.length > 0) {
-      this.callback(this._searchResults[this._searchIndex]);
+    if (this._searchResults && this._searchResults.length > 0) {
+      this._lastQuery = this._selectResult(this._searchIndex);
     }
   },
 
   /**
    * Handles keypress and mouse click on the suggestions richlistbox.
    */
   _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
     switch(aEvent.keyCode || aEvent.button) {
@@ -437,16 +469,19 @@ SelectorSearch.prototype = {
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function SelectorSearch_showSuggestions() {
+    if (!this.walker.isLocal()) {
+      return;
+    }
     let query = this.searchBox.value;
     if (this._lastValidSearch != "" &&
         this._lastToLastValidSearch != this._lastValidSearch) {
       this._searchSuggestions = {
         ids: new Map(),
         classes: new Map(),
         tags: new Map(),
       };
--- a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
@@ -63,16 +63,17 @@ function test()
   {
     openInspector(startTest);
   }
 
   function startTest(aInspector)
   {
     inspector = aInspector;
     inspector.selection.setNode($("b1"));
+
     searchBox =
       inspector.panelWin.document.getElementById("inspector-searchbox");
 
     focusSearchBoxUsingShortcut(inspector.panelWin, function() {
       searchBox.addEventListener("command", checkState, true);
       searchBox.addEventListener("keypress", checkState, true);
       checkStateAndMoveOn(0);
     });
@@ -90,25 +91,28 @@ function test()
     info("pressing key " + key + " to get id " + id);
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
     if (event.type == "keypress" && keypressStates.indexOf(state) == -1) {
       return;
     }
-    executeSoon(function() {
-      let [key, id, isValid] = keyStates[state];
-      info(inspector.selection.node.id + " is selected with text " +
-           inspector.searchBox.value);
-      is(inspector.selection.node, $(id),
-         "Correct node is selected for state " + state);
-      is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
-         "Correct searchbox result state for state " + state);
-      checkStateAndMoveOn(state + 1);
+
+    inspector.searchSuggestions._lastQuery.then(() => {
+      executeSoon(() => {
+        let [key, id, isValid] = keyStates[state];
+        info(inspector.selection.node.id + " is selected with text " +
+             inspector.searchBox.value);
+        is(inspector.selection.node, $(id),
+           "Correct node is selected for state " + state);
+        is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
+           "Correct searchbox result state for state " + state);
+        checkStateAndMoveOn(state + 1);
+      });
     });
   }
 
   function finishUp() {
     searchBox = null;
     gBrowser.removeCurrentTab();
     finish();
   }
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
@@ -55,16 +55,17 @@ function test()
   function setupTest()
   {
     openInspector(startTest);
   }
 
   function startTest(aInspector)
   {
     inspector = aInspector;
+
     searchBox =
       inspector.panelWin.document.getElementById("inspector-searchbox");
     popup = inspector.searchSuggestions.searchPopup;
 
     focusSearchBoxUsingShortcut(inspector.panelWin, function() {
       searchBox.addEventListener("command", checkState, true);
       checkStateAndMoveOn(0);
     });
@@ -80,17 +81,17 @@ function test()
     state = index;
 
     info("pressing key " + key + " to get suggestions " +
          JSON.stringify(suggestions));
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
-    executeSoon(function() {
+    inspector.searchSuggestions._lastQuery.then(() => {
       let [key, suggestions] = keyStates[state];
       let actualSuggestions = popup.getItems();
       is(popup._panel.state == "open" || popup._panel.state == "showing"
          ? actualSuggestions.length: 0, suggestions.length,
          "There are expected number of suggestions at " + state + "th step.");
       actualSuggestions = actualSuggestions.reverse();
       for (let i = 0; i < suggestions.length; i++) {
         is(suggestions[i][0], actualSuggestions[i].label,
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
@@ -82,17 +82,17 @@ function test()
     state = index;
 
     info("pressing key " + key + " to get suggestions " +
          JSON.stringify(suggestions));
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
-    executeSoon(function() {
+    inspector.searchSuggestions._lastQuery.then(() => {
       let [key, suggestions] = keyStates[state];
       let actualSuggestions = popup.getItems();
       is(popup._panel.state == "open" || popup._panel.state == "showing"
          ? actualSuggestions.length: 0, suggestions.length,
          "There are expected number of suggestions at " + state + "th step.");
       actualSuggestions = actualSuggestions.reverse();
       for (let i = 0; i < suggestions.length; i++) {
         is(suggestions[i][0], actualSuggestions[i].label,
--- a/browser/devtools/inspector/test/browser_inspector_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_menu.js
@@ -98,17 +98,17 @@ function test() {
                      function() { copyUniqueSelector.doCommand(); },
                      testDeleteNode, testDeleteNode);
   }
 
   function testDeleteNode() {
     let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
     ok(deleteNode, "the popup menu has a delete menu item");
 
-    inspector.selection.once("detached", deleteTest);
+    inspector.once("markupmutation", deleteTest);
 
     let commandEvent = document.createEvent("XULCommandEvent");
     commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
                                   false, false, null);
     deleteNode.dispatchEvent(commandEvent);
   }
 
   function deleteTest() {
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -225,38 +225,38 @@ MarkupView.prototype = {
   },
 
   /**
    * Delete a node from the DOM.
    * This is an undoable action.
    */
   deleteNode: function MC__deleteNode(aNode)
   {
-    aNode = aNode.rawNode();
-    if (!aNode) {
-      return;
-    }
-    let doc = nodeDocument(aNode);
-    if (aNode === doc ||
-        aNode === doc.documentElement ||
+    if (aNode.isDocumentElement ||
         aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
       return;
     }
 
-    let parentNode = aNode.parentNode;
-    let sibling = aNode.nextSibling;
+    let container = this._containers.get(aNode);
 
-    this.undo.do(function() {
-      if (aNode.selected) {
-        this.navigate(this._containers.get(parentNode));
-      }
-      parentNode.removeChild(aNode);
-    }.bind(this), function() {
-      parentNode.insertBefore(aNode, sibling);
-    });
+    // Retain the node so we can undo this...
+    this.walker.retainNode(aNode).then(() => {
+      let parent = aNode.parentNode();
+      let sibling = null;
+      this.undo.do(() => {
+        if (container.selected) {
+          this.navigate(this._containers.get(parent));
+        }
+        this.walker.removeNode(aNode).then(nextSibling => {
+          sibling = nextSibling;
+        });
+      }, () => {
+        this.walker.insertBefore(aNode, parent, sibling);
+      });
+    }).then(null, console.error);
   },
 
   /**
    * If an editable item is focused, select its container.
    */
   _onFocus: function MC__onFocus(aEvent) {
     let parent = aEvent.target;
     while (!parent.container) {
@@ -588,16 +588,19 @@ MarkupView.prototype = {
     // centered node.
     let centered = this._checkSelectionVisible(aContainer);
 
     // Children aren't updated yet, but clear the childrenDirty flag anyway.
     // If the dirty flag is re-set while we're fetching we'll need to fetch
     // again.
     aContainer.childrenDirty = false;
     let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => {
+      if (!this._containers) {
+        return promise.reject("markup view destroyed");
+      }
       this._queuedChildUpdates.delete(aContainer);
 
       // If children are dirty, we got a change notification for this node
       // while the request was in progress, we need to do it again.
       if (aContainer.childrenDirty) {
         return this._updateChildren(aContainer, centered);
       }
 
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -6,514 +6,325 @@
 # command line which is available from the Web Developer sub-menu
 # -> '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 (canonDescNone): Short string used to describe any command
-# or command parameter when no description has been provided.
+# For each command there are in general two strings. As an example consider
+# the 'pref' command.
+# commandDesc (e.g. prefDesc for the command 'pref'): this string contains a
+# very short description of the command. It's designed to be shown in a menu
+# alongside the command name, which is why it should be as short as possible.
+# commandManual (e.g. prefManual for the command 'pref'): this string will
+# contain a fuller description of the command. It's diplayed when the user
+# asks for help about a specific command (e.g. 'help pref').
+
+# LOCALIZATION NOTE: This message is 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.
+# LOCALIZATION NOTE: 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.
+# LOCALIZATION NOTE (canonProxyDesc, canonProxyManual): These commands are
+# used to execute commands on a remote system (using a proxy). Parameters: %S
+# is the name of the remote system.
 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.
+# LOCALIZATION NOTE: This error message is displayed when we try to add a new
+# command (using a proxy) where one already exists with the same 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.
+# LOCALIZATION NOTE: This message describes the '{' command, which allows
+# entry of JavaScript like traditional developer tool command lines.
 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.
+# LOCALIZATION NOTE: This message is displayed when the command line has more
+# arguments than the current command can understand.
 cliUnusedArg=Too many arguments
 
-# LOCALIZATION NOTE (cliOptions): The title of the dialog which displays the
-# options that are available to the current command.
+# LOCALIZATION NOTE: The title of the dialog which displays the options that
+# are available to the current command.
 cliOptions=Available Options
 
-# LOCALIZATION NOTE (fieldSelectionSelect): When a command has a parameter
-# that has a number of pre-defined options the user interface presents these
-# in a drop-down menu, where the first 'option' is an indicator that a
-# selection should be made. This string describes that first option.
+# LOCALIZATION NOTE: Error message given when a file argument points to a file
+# that does not exist, but should (e.g. for use with File->Open) %1$S is a
+# filename
+fileErrNotExists='%1$S' doesn't exist
+
+# LOCALIZATION NOTE: Error message given when a file argument points to a file
+# that exists, but should not (e.g. for use with File->Save As) %1$S is a
+# filename
+fileErrExists='%1$S' already exists
+
+# LOCALIZATION NOTE: Error message given when a file argument points to a
+# non-file, when a file is needed. %1$S is a filename
+fileErrIsNotFile='%1$S' is not a file
+
+# LOCALIZATION NOTE: Error message given when a file argument points to a
+# non-directory, when a directory is needed (e.g. for use with 'cd') %1$S is a
+# filename
+fileErrIsNotDirectory='%1$S' is not a directory
+
+# LOCALIZATION NOTE: Error message given when a file argument does not match
+# the specified regular expression %1$S is a filename %2$S is a regular
+# expression
+fileErrDoesntMatch='%1$S' does not match '%2$S'
+
+# LOCALIZATION NOTE: When a command has a parameter that has a number of
+# pre-defined options the user interface presents these in a drop-down menu,
+# where the first 'option' is an indicator that a selection should be made.
+# This string describes that first option.
 fieldSelectionSelect=Select a %S…
 
-# LOCALIZATION NOTE (fieldArrayAdd): When a command has a parameter that can
-# be repeated a number of times (e.g. like the 'cat a.txt b.txt' command) the
-# user interface presents buttons to add and remove arguments. This string is
-# used to add arguments.
+# LOCALIZATION NOTE (fieldArrayAdd, fieldArrayDel): When a command has a
+# parameter that can be repeated multiple times (e.g. like the 'cat a.txt
+# b.txt' command) the user interface presents buttons to add and remove
+# arguments. This string is used to add arguments.
 fieldArrayAdd=Add
-
-# LOCALIZATION NOTE (fieldArrayDel): When a command has a parameter that can
-# be repeated a number of times (e.g. like the 'cat a.txt b.txt' command) the
-# user interface presents buttons to add and remove arguments. This string is
-# used to remove arguments.
 fieldArrayDel=Delete
 
-# LOCALIZATION NOTE (fieldMenuMore): When the menu has displayed all the
-# matches that it should (i.e. about 10 items) then we display this to alert
-# the user that more matches are available.
+# LOCALIZATION NOTE: When the menu has displayed all the matches that it
+# should (i.e. about 10 items) then we display this to alert the user that
+# more matches are available.
 fieldMenuMore=More matches, keep typing
 
-# LOCALIZATION NOTE (jstypeParseScope): The command line provides completion
-# for JavaScript commands, however there are times when the scope of what
-# we're completing against can't be used. This error message is displayed when
-# this happens.
+# LOCALIZATION NOTE: The command line provides completion for JavaScript
+# commands, however there are times when the scope of what we're completing
+# against can't be used. This error message is displayed when this happens.
 jstypeParseScope=Scope lost
 
-# LOCALIZATION NOTE (jstypeParseMissing): When the command line is doing
-# JavaScript completion, sometimes the property to be completed does not
-# exist. This error message is displayed when this happens.
+# LOCALIZATION NOTE (jstypeParseMissing, jstypeBeginSyntax,
+# jstypeBeginUnterm): These error messages are displayed when the command line
+# is doing JavaScript completion and encounters errors.
 jstypeParseMissing=Can't find property '%S'
-
-# LOCALIZATION NOTE (jstypeBeginSyntax): When the command line is doing
-# JavaScript completion using invalid JavaScript, this error message is
-# displayed.
 jstypeBeginSyntax=Syntax error
-
-# LOCALIZATION NOTE (jstypeBeginUnterm): When the command line is doing
-# JavaScript completion using a string that is not properly terminated, this
-# error message is displayed.
 jstypeBeginUnterm=Unterminated string literal
 
-# LOCALIZATION NOTE (jstypeParseError): If the system for providing JavaScript
-# completions encounters and error it displays this.
+# LOCALIZATION NOTE: This message is displayed if the system for providing
+# JavaScript completions encounters and error it displays this.
 jstypeParseError=Error
 
-# LOCALIZATION NOTE (typesNumberNan): When the command line is passed a
-# number, however the input string is not a valid number, this error message
-# is displayed.
+# LOCALIZATION NOTE (typesNumberNan, typesNumberNotInt2, typesDateNan): These
+# error messages are displayed when the command line is passed a variable
+# which has the wrong format and can't be converted. Parameters: %S is the
+# passed variable.
 typesNumberNan=Can't convert "%S" to a number.
-
-# LOCALIZATION NOTE (typesNumberMax): When the command line is passed a
-# number, but the number is bigger than the largest allowed number, this error
-# message is displayed.
-typesNumberMax=%1$S is greater than maximum allowed: %2$S.
-
-# LOCALIZATION NOTE (typesNumberMin): When the command line is passed a
-# number, but the number is lower than the smallest allowed number, this error
-# message is displayed.
-typesNumberMin=%1$S is smaller than minimum allowed: %2$S.
-
-# LOCALIZATION NOTE (typesNumberNotInt2): When the command line is passed a
-# number, but the number has a decimal part and floats are not allowed.
 typesNumberNotInt2=Can't convert "%S" to an integer.
-
-# LOCALIZATION NOTE (typesDateNan): When the command line is passed a date,
-# however the input string is not a valid date, this error message is
-# displayed.
 typesDateNan=Can't convert "%S" to a date.
 
-# LOCALIZATION NOTE (typesDateMax): When the command line is passed a date,
-# but the number is later than the latest allowed date, this error message is
-# displayed.
+# LOCALIZATION NOTE (typesNumberMax, typesNumberMin, typesDateMax,
+# typesDateMin): These error messages are displayed when the command line is
+# passed a variable which has a value out of range (number or date).
+# Parameters: %1$S is the passed variable, %2$S is the limit value.
+typesNumberMax=%1$S is greater than maximum allowed: %2$S.
+typesNumberMin=%1$S is smaller than minimum allowed: %2$S.
 typesDateMax=%1$S is later than maximum allowed: %2$S.
-
-# LOCALIZATION NOTE (typesDateMin): When the command line is passed a date,
-# but the date is earlier than the earliest allowed number, this error message
-# is displayed.
 typesDateMin=%1$S is earlier than minimum allowed: %2$S.
 
-# LOCALIZATION NOTE (typesSelectionNomatch): When the command line is passed
-# an option with a limited number of correct values, but the passed value is
-# not one of them, this error message is displayed.
+# LOCALIZATION NOTE: This error message is displayed when the command line is
+# passed an option with a limited number of correct values, but the passed
+# value is not one of them.
 typesSelectionNomatch=Can't use '%S'.
 
-# LOCALIZATION NOTE (nodeParseSyntax): When the command line is expecting a
-# CSS query string, however the passed string is not valid, this error message
-# is displayed.
+# LOCALIZATION NOTE: This error message is displayed when the command line is
+# expecting a CSS query string, however the passed string is not valid.
 nodeParseSyntax=Syntax error in CSS query
 
-# LOCALIZATION NOTE (nodeParseMultiple): When the command line is expecting a
-# CSS string that matches a single node, but more than one node matches, this
-# error message is displayed.
+# LOCALIZATION NOTE (nodeParseMultiple, nodeParseNone): These error messages
+# are displayed when the command line is expecting a CSS string that matches a
+# single node, but more nodes (or none) match.
 nodeParseMultiple=Too many matches (%S)
-
-# LOCALIZATION NOTE (nodeParseNone): When the command line is expecting a CSS
-# string that matches a single node, but no nodes match, this error message is
-# displayed.
 nodeParseNone=No matches
 
-# LOCALIZATION NOTE (helpDesc): A very short description of the 'help'
-# 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 helpManual for a
-# fuller description of what it does.
+# LOCALIZATION NOTE (helpDesc, helpManual, helpSearchDesc, helpSearchManual3):
+# These strings describe the "help" command, used to display a description of
+# a command (e.g. "help pref"), and its parameter 'search'.
 helpDesc=Get help on the available commands
-
-# LOCALIZATION NOTE (helpManual): A fuller description of the 'help' command.
-# Displayed when the user asks for help on what it does.
 helpManual=Provide help either on a specific command (if a search string is provided and an exact match is found) or on the available commands (if a search string is not provided, or if no exact match is found).
-
-# LOCALIZATION NOTE (helpSearchDesc): A very short description of the 'search'
-# parameter to the 'help' command. See helpSearchManual3 for a fuller
-# description of what it does. This string is designed to be shown in a dialog
-# with restricted space, which is why it should be as short as possible.
 helpSearchDesc=Search string
-
-# LOCALIZATION NOTE (helpSearchManual3): A fuller description of the 'search'
-# parameter to the 'help' command. Displayed when the user asks for help on
-# what it does.
 helpSearchManual3=search string to use in narrowing down the displayed commands. Regular expressions not supported.
 
-# LOCALIZATION NOTE (helpManSynopsis): A heading shown at the top of a help
-# page for a command in the console It labels a summary of the parameters to
-# the command
+# LOCALIZATION NOTE (helpManSynopsis, helpManDescription, helpManParameters):
+# These strings are displayed in the help page for a command in the console.
 helpManSynopsis=Synopsis
-
-# LOCALIZATION NOTE (helpManDescription): A heading shown in a help page for a
-# command in the console. This heading precedes the top level description.
 helpManDescription=Description
-
-# LOCALIZATION NOTE (helpManParameters): A heading shown above the parameters
-# in a help page for a command in the console.
 helpManParameters=Parameters
 
-# LOCALIZATION NOTE (helpManNone): Some text shown under the parameters
-# heading in a help page for a command which has no parameters.
+# LOCALIZATION NOTE: This message is displayed in the help page if the command
+# has no parameters.
 helpManNone=None
 
-# LOCALIZATION NOTE (helpListAll): The heading shown in response to the 'help'
+# LOCALIZATION NOTE: This message is displayed in response to the 'help'
 # command when used without a filter, just above the list of known commands.
 helpListAll=Available Commands:
 
-# LOCALIZATION NOTE (helpListPrefix): The heading shown in response to the
-# 'help <search>' command (i.e. with a search string), just above the list of
-# matching commands.
-helpListPrefix=Commands starting with '%1$S':
-
-# LOCALIZATION NOTE (helpListNone): The heading shown in response to the 'help
-# <search>' command (i.e. with a search string), when there are no matching
-# commands.
-helpListNone=No commands starting with '%1$S'
+# LOCALIZATION NOTE (helpListPrefix, helpListNone): These messages are
+# displayed in response to the 'help <search>' command (i.e. with a search
+# string), just above the list of matching commands. Parameters: %S is the
+# search string.
+helpListPrefix=Commands starting with '%S':
+helpListNone=No commands starting with '%S'
 
-# LOCALIZATION NOTE (helpManRequired): When the 'help x' command wants to show
-# the manual for the 'x' command it needs to be able to describe the
-# parameters as either required or optional, or if they have a default value.
-# See also 'helpManOptional' and 'helpManDefault'.
+# LOCALIZATION NOTE (helpManRequired, helpManOptional, helpManDefault): When
+# the 'help x' command wants to show the manual for the 'x' command, it needs
+# to be able to describe the parameters as either required or optional, or if
+# they have a default value.
 helpManRequired=required
-
-# LOCALIZATION NOTE (helpManOptional): See description of 'helpManRequired'
 helpManOptional=optional
+helpManDefault=optional, default=%S
 
-# LOCALIZATION NOTE (helpManDefault): See description of 'helpManRequired'. %1$
-# S is the default value
-helpManDefault=optional, default=%1$S
-
-# LOCALIZATION NOTE (subCommands): Text shown as part of the output of the
-# 'help' command when the command in question has sub-commands, before a list
-# of the matching sub-commands
+# LOCALIZATION NOTE: Text shown as part of the output of the 'help' command
+# when the command in question has sub-commands, before a list 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
+# LOCALIZATION NOTE: 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.
+# LOCALIZATION NOTE (contextDesc, contextManual, contextPrefixDesc): These
+# strings are used to describe the 'context' command and its 'prefix'
+# parameter. See localization comment for 'connect' for an explanation about
+# 'prefix'.
 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: This message message displayed during the processing of
+# the 'context' command, when the found command is not a parent command.
+contextNotParentError=Can't use '%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
+# LOCALIZATION NOTE (contextReply, contextEmptyReply): These messages are
+# displayed during the processing of the 'context' command, to indicate
+# success or that there is no command prefix.
+contextReply=Using %S as a 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.
+# LOCALIZATION NOTE (connectDesc, connectManual, connectPrefixDesc,
+# connectPortDesc, connectHostDesc, connectDupReply): These strings describe
+# the 'connect' command and all its available parameters. A 'prefix' is an 
+# alias for the remote server (think of it as a "connection name"), and it
+# allows to identify a specific server when connected to multiple remote
+# servers.
 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.
+# LOCALIZATION NOTE: The output of the 'connect' command, telling the user
+# what it has done. Parameters: %S is the prefix command. See localization
+# comment for 'connect' for an explanation about 'prefix'.
 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.
+# LOCALIZATION NOTE (disconnectDesc2, disconnectManual2, disconnectPrefixDesc,
+# disconnectForceDesc): These strings describe the 'disconnect' command and
+# all its available parameters. See localization comment for 'connect' for an
+# explanation about 'prefix'.
+disconnectDesc2=Disconnect from server
+disconnectManual2=Disconnect from a server currently connected for remote commands execution
 disconnectPrefixDesc=Parent prefix for imported commands
-
-# LOCALIZATION NOTE (disconnectForceDesc): A short description of the 'force'
-# 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.
 disconnectForceDesc=Ignore outstanding requests
 
-# LOCALIZATION NOTE (disconnectReply): The output of the 'disconnect' command,
-# telling the user what it's done.
+# LOCALIZATION NOTE: This is the output of the 'disconnect' command,
+# explaining the user what has been done. Parameters: %S is the number of
+# commands removed.
 disconnectReply=Removed %S commands.
 
-# LOCALIZATION NOTE (disconnectOutstanding): An error message displayed when
-# the user attempts to disconnect before all requests have completed. %1$S is
-# a list of commands which are incomplete
-disconnectOutstanding=Outstanding requests (%1$S)
-
-# 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.
-prefManual=Commands to display and alter preferences both for GCLI and the surrounding environment
-
-# LOCALIZATION NOTE (prefListDesc): A very short description of the 'pref
-# list' 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
-# prefListManual for a fuller description of what it does.
-prefListDesc=Display available settings
-
-# LOCALIZATION NOTE (prefListManual): A fuller description of the 'pref list'
-# command. Displayed when the user asks for help on what it does.
-prefListManual=Display a list of preferences, optionally filtered when using the 'search' parameter
+# LOCALIZATION NOTE: This error message is displayed when the user attempts to
+# disconnect before all requests have completed. Parameters: %S is a list of
+# incomplete requests.
+disconnectOutstanding=Outstanding requests (%S)
 
-# LOCALIZATION NOTE (prefListSearchDesc): A short description of the 'search'
-# parameter to the 'pref list' command. See prefListSearchManual for a fuller
-# description of what it does. This string is designed to be shown in a dialog
-# with restricted space, which is why it should be as short as possible.
+# LOCALIZATION NOTE (prefDesc, prefManual, prefListDesc, prefListManual,
+# prefListSearchDesc, prefListSearchManual, prefShowDesc, prefShowManual,
+# prefShowSettingDesc, prefShowSettingManual): These strings describe the
+# 'pref' command and all its available sub-commands and parameters.
+prefDesc=Commands to control settings
+prefManual=Commands to display and alter preferences both for GCLI and the surrounding environment
+prefListDesc=Display available settings
+prefListManual=Display a list of preferences, optionally filtered when using the 'search' parameter
 prefListSearchDesc=Filter the list of settings displayed
-
-# LOCALIZATION NOTE (prefListSearchManual): A fuller description of the
-# 'search' parameter to the 'pref list' command. Displayed when the user asks
-# for help on what it does.
 prefListSearchManual=Search for the given string in the list of available preferences
-
-# LOCALIZATION NOTE (prefShowDesc): A very short description of the 'pref
-# show' 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
-# prefShowManual for a fuller description of what it does.
 prefShowDesc=Display setting value
-
-# LOCALIZATION NOTE (prefShowManual): A fuller description of the 'pref show'
-# command. Displayed when the user asks for help on what it does.
 prefShowManual=Display the value of a given preference
-
-# LOCALIZATION NOTE (prefShowSettingDesc): A short description of the
-# 'setting' parameter to the 'pref show' command. See prefShowSettingManual
-# for a fuller description of what it does. This string is designed to be
-# shown in a dialog with restricted space, which is why it should be as short
-# as possible.
 prefShowSettingDesc=Setting to display
-
-# LOCALIZATION NOTE (prefShowSettingManual): A fuller description of the
-# 'setting' parameter to the 'pref show' command. Displayed when the user asks
-# for help on what it does.
 prefShowSettingManual=The name of the setting to display
 
-# LOCALIZATION NOTE (prefShowSettingValue): This is used to show the
-# preference name and the associated preference value. %1$S is replaced with
-# the preference name and %2$S is replaced with the preference value.
+# LOCALIZATION NOTE: This message is used to show the preference name and the
+# associated preference value. Parameters: %1$S is the preference name, %2$S
+# is the preference value.
 prefShowSettingValue=%1$S: %2$S
 
-# LOCALIZATION NOTE (prefSetDesc): A very short description of the 'pref set'
-# 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 prefSetManual for
-# a fuller description of what it does.
+# LOCALIZATION NOTE (prefSetDesc, prefSetManual, prefSetSettingDesc,
+# prefSetSettingManual, prefSetValueDesc, prefSetValueManual): These strings
+# describe the 'pref set' command and all its parameters.
 prefSetDesc=Alter a setting
-
-# LOCALIZATION NOTE (prefSetManual): A fuller description of the 'pref set'
-# command. Displayed when the user asks for help on what it does.
 prefSetManual=Alter preferences defined by the environment
-
-# LOCALIZATION NOTE (prefSetSettingDesc): A short description of the 'setting'
-# parameter to the 'pref set' command. See prefSetSettingManual for a fuller
-# description of what it does. This string is designed to be shown in a dialog
-# with restricted space, which is why it should be as short as possible.
 prefSetSettingDesc=Setting to alter
-
-# LOCALIZATION NOTE (prefSetSettingManual): A fuller description of the
-# 'setting' parameter to the 'pref set' command. Displayed when the user asks
-# for help on what it does.
 prefSetSettingManual=The name of the setting to alter.
-
-# LOCALIZATION NOTE (prefSetValueDesc): A short description of the 'value'
-# parameter to the 'pref set' command. See prefSetValueManual for a fuller
-# description of what it does. This string is designed to be shown in a dialog
-# with restricted space, which is why it should be as short as possible.
 prefSetValueDesc=New value for setting
-
-# LOCALIZATION NOTE (prefSetValueManual): A fuller description of the 'value'
-# parameter to the 'pref set' command. Displayed when the user asks for help
-# on what it does.
 prefSetValueManual=The new value for the specified setting
 
-# LOCALIZATION NOTE (prefSetCheckHeading): Title displayed to the user the
-# first time they try to alter a setting This is displayed directly above
-# prefSetCheckBody and prefSetCheckGo.
+# LOCALIZATION NOTE (prefSetCheckHeading, prefSetCheckBody, prefSetCheckGo):
+# These strings are displayed to the user the first time they try to alter a
+# setting.
 prefSetCheckHeading=This might void your warranty!
-
-# LOCALIZATION NOTE (prefSetCheckBody): The main text of the warning displayed
-# to the user the first time they try to alter a setting. See also
-# prefSetCheckHeading and prefSetCheckGo.
 prefSetCheckBody=Changing these advanced settings can be harmful to the stability, security, and performance of this application. You should only continue if you are sure of what you are doing.
-
-# LOCALIZATION NOTE (prefSetCheckGo): The text to enable preference editing.
-# Displayed in a button directly under prefSetCheckHeading and
-# prefSetCheckBody
 prefSetCheckGo=I'll be careful, I promise!
 
-# LOCALIZATION NOTE (prefResetDesc): A very short description of the 'pref
-# reset' 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
-# prefResetManual for a fuller description of what it does.
+# LOCALIZATION NOTE (prefResetDesc, prefResetManual, prefResetSettingDesc,
+# prefResetSettingManual): These strings describe the 'pref reset' command and
+# all its parameters.
 prefResetDesc=Reset a setting
-
-# LOCALIZATION NOTE (prefResetManual): A fuller description of the 'pref
-# reset' command. Displayed when the user asks for help on what it does.
 prefResetManual=Reset the value of a setting to the system defaults
-
-# LOCALIZATION NOTE (prefResetSettingDesc): A short description of the
-# 'setting' parameter to the 'pref reset' command. See prefResetSettingManual
-# for a fuller description of what it does. This string is designed to be
-# shown in a dialog with restricted space, which is why it should be as short
-# as possible.
 prefResetSettingDesc=Setting to reset
-
-# LOCALIZATION NOTE (prefResetSettingManual): A fuller description of the
-# 'setting' parameter to the 'pref reset' command. Displayed when the user
-# asks for help on what it does.
 prefResetSettingManual=The name of the setting to reset to the system default value
 
-# LOCALIZATION NOTE (prefOutputFilter): Displayed in the output from the 'pref
+# LOCALIZATION NOTE: This string is displayed in the output from the 'pref
 # list' command as a label to an input element that allows the user to filter
-# the results
+# the results.
 prefOutputFilter=Filter
 
-# LOCALIZATION NOTE (prefOutputName): Displayed in the output from the 'pref
-# list' command as a heading to a table. The column contains the names of the
-# available preferences
+# LOCALIZATION NOTE (prefOutputName, prefOutputValue): These strings are
+# displayed in the output from the 'pref list' command as table headings.
 prefOutputName=Name
-
-# LOCALIZATION NOTE (prefOutputValue): Displayed in the output from the 'pref
-# list' command as a heading to a table. The column contains the values of the
-# available preferences
 prefOutputValue=Value
 
-# LOCALIZATION NOTE (introDesc): A very short description of the 'intro'
-# 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 introManual for a
-# fuller description of what it does.
+# LOCALIZATION NOTE (introDesc, introManual): These strings describe the
+# 'intro' command. The localization of 'Got it!' should be the same used in
+# introTextGo.
 introDesc=Show the opening message
-
-# LOCALIZATION NOTE (introManual): A fuller description of the 'intro'
-# command. Displayed when the user asks for help on what it does.
 introManual=Redisplay the message that is shown to new users until they click the 'Got it!' button
 
-# LOCALIZATION NOTE (introTextOpening2): The 'intro text' opens when the user
+# LOCALIZATION NOTE (introTextOpening2, introTextCommands, introTextKeys2,
+# introTextF1Escape, introTextGo): These strings are displayed when the user
 # first opens the developer toolbar to explain the command line, and is shown
-# each time it is opened until the user clicks the 'Got it!' button. This
-# string is the opening paragraph of the intro text.
+# each time it is opened until the user clicks the 'Got it!' button.
 introTextOpening2=This command line is designed for developers. It focuses on speed of input over JavaScript syntax and a rich display over monospace output.
-
-# LOCALIZATION NOTE (introTextCommands): For information about the 'intro
-# text' see introTextOpening2. The second paragraph is in 2 sections, the
-# first section points the user to the 'help' command.
 introTextCommands=For a list of commands type
-
-# LOCALIZATION NOTE (introTextKeys2): For information about the 'intro text'
-# see introTextOpening2. The second section in the second paragraph points the
-# user to the F1/Escape keys which show and hide hints.
 introTextKeys2=, or to show/hide command hints press
-
-# LOCALIZATION NOTE (introTextF1Escape): For information about the 'intro
-# text' see introTextOpening2. This string is used with introTextKeys2, and
-# contains the keys that are pressed to open and close hints.
 introTextF1Escape=F1/Escape
-
-# LOCALIZATION NOTE (introTextGo): For information about the 'intro text' see
-# introTextOpening2. The text on the button that dismisses the intro text.
 introTextGo=Got it!
 
-# LOCALIZATION NOTE (hideIntroDesc): Short description of the 'hideIntro'
-# setting. Displayed when the user asks for help on the settings.
+# LOCALIZATION NOTE: This is a short description of the 'hideIntro' setting.
 hideIntroDesc=Show the initial welcome message
 
-# LOCALIZATION NOTE (eagerHelperDesc): Short description of the 'eagerHelper'
-# setting. Displayed when the user asks for help on the settings. eagerHelper
-# allows users to select between showing no tooltips, permanent tooltips, and
-# only important tooltips
+# LOCALIZATION NOTE: This is a description of the 'eagerHelper' setting. It's
+# displayed when the user asks for help on the settings. eagerHelper allows
+# users to select between showing no tooltips, permanent tooltips, and only
+# important tooltips.
 eagerHelperDesc=How eager are the tooltips
 
-# LOCALIZATION NOTE (allowSetDesc): Short description of the 'allowSetDesc'
-# setting. Displayed when the user asks for help on the settings.
+# LOCALIZATION NOTE: This is a short description of the 'allowSetDesc'
+# setting.
 allowSetDesc=Has the user enabled the 'pref set' command?
 
-# LOCALIZATION NOTE (introBody): The text displayed at the top of the output
-# for the help command, just before the list of commands. This text is wrapped
-# inside a link to a localized MDN article
+# LOCALIZATION NOTE: This text is displayed at the top of the output for the
+# help command, just before the list of commands. This text is wrapped inside
+# a link to a localized MDN article.
 introBody=For more information see MDN.
-
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -499,16 +499,31 @@ resizeModeManual2=Responsive websites re
 # name, which is why it should be as short as possible.
 cmdDesc=Manipulate the commands
 
 # LOCALIZATION NOTE (cmdRefreshDesc) A very short description of the 'cmd refresh'
 # 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.
 cmdRefreshDesc=Re-read mozcmd directory
 
+# LOCALIZATION NOTE (cmdStatus) When the we load new commands from mozcmd
+# directory, we report on how many we loaded. %1$S is a count of the number
+# of loaded commands, and %2$S is the directory we loaded from.
+cmdStatus=Read %1$S commands from '%2$S'
+
+# LOCALIZATION NOTE (cmdSetdirDesc) 
+cmdSetdirDesc=Setup a mozcmd directory
+
+# LOCALIZATION NOTE (cmdSetdirManual) 
+cmdSetdirManual=A 'mozcmd' directory is an easy way to create new custom commands for the Firefox command line. For more information see the <a href="https://developer.mozilla.org/en-US/docs/Tools/GCLI/Customization">MDN documentation</a>.
+
+# LOCALIZATION NOTE (cmdSetdirDirectoryDesc) The description of the directory
+# parameter to the 'cmd setdir' command.
+cmdSetdirDirectoryDesc=Directory containing .mozcmd files
+
 # LOCALIZATION NOTE (addonDesc) A very short description of the 'addon'
 # 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.
 addonDesc=Manipulate add-ons
 
 # LOCALIZATION NOTE (addonListDesc) A very short description of the 'addon list'
 # 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.
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -4,16 +4,18 @@
 
 <!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings -->
 <!-- LOCALIZATION NOTE : FILE Do not translate key -->
 
 <!ENTITY closeCmd.key  "W">
 
 <!ENTITY toolboxCloseButton.tooltip    "Close Developer Tools">
 <!ENTITY toolboxOptionsButton.key      "O">
+<!ENTITY toolboxNextTool.key           "]">
+<!ENTITY toolboxPreviousTool.key       "[">
 
 <!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for
   -  the heading of the advanced settings group in the options panel. -->
 <!ENTITY options.context.advancedSettings "Advanced settings">
 
 <!-- LOCALIZATION NOTE (options.context.requiresRestart2): This is the requires
   -  restart label at right of settings that require a browser restart to be
   -  effective. -->
--- a/toolkit/devtools/gcli/gcli.jsm
+++ b/toolkit/devtools/gcli/gcli.jsm
@@ -46,601 +46,129 @@ var Event = Components.interfaces.nsIDOM
  *
  * 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.
  */
 
-var mozl10n = {};
-
-(function(aMozl10n) {
-
-  'use strict';
-
-  var temp = {};
-  Components.utils.import("resource://gre/modules/Services.jsm", temp);
-
-  var stringBundle;
-  try {
-    stringBundle = temp.Services.strings.createBundle(
-            "chrome://browser/locale/devtools/gclicommands.properties");
-  }
-  catch (ex) {
-    console.error("Using string fallbacks");
-    stringBundle = {
-      GetStringFromName: function(name) { return name; },
-      formatStringFromName: function(name) { return name; }
-    };
-  }
+define('gcli/index', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/api', 'gcli/types/selection', 'gcli/types/delegate', 'gcli/types/array', 'gcli/types/boolean', 'gcli/types/command', 'gcli/types/date', 'gcli/types/file', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/number', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/string', 'gcli/converters', 'gcli/converters/basic', 'gcli/converters/terminal', '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/ui/ffdisplay'], function(require, exports, module) {
+
+'use strict';
+
+require('gcli/settings').startup();
+
+var api = require('gcli/api');
+api.populateApi(exports);
+
+exports.addItems(require('gcli/types/selection').items);
+exports.addItems(require('gcli/types/delegate').items);
+
+exports.addItems(require('gcli/types/array').items);
+exports.addItems(require('gcli/types/boolean').items);
+exports.addItems(require('gcli/types/command').items);
+exports.addItems(require('gcli/types/date').items);
+exports.addItems(require('gcli/types/file').items);
+exports.addItems(require('gcli/types/javascript').items);
+exports.addItems(require('gcli/types/node').items);
+exports.addItems(require('gcli/types/number').items);
+exports.addItems(require('gcli/types/resource').items);
+exports.addItems(require('gcli/types/setting').items);
+exports.addItems(require('gcli/types/string').items);
+
+exports.addItems(require('gcli/converters').items);
+exports.addItems(require('gcli/converters/basic').items);
+// Don't export the 'html' type to avoid use of innerHTML
+// exports.addItems(require('gcli/converters/html').items);
+exports.addItems(require('gcli/converters/terminal').items);
+
+exports.addItems(require('gcli/ui/intro').items);
+exports.addItems(require('gcli/ui/focus').items);
+
+exports.addItems(require('gcli/ui/fields/basic').items);
+exports.addItems(require('gcli/ui/fields/javascript').items);
+exports.addItems(require('gcli/ui/fields/selection').items);
+
+// Don't export the '{' command
+// exports.addItems(require('gcli/cli').items);
+
+exports.addItems(require('gcli/commands/connect').items);
+exports.addItems(require('gcli/commands/context').items);
+exports.addItems(require('gcli/commands/help').items);
+exports.addItems(require('gcli/commands/pref').items);
+
+/**
+ * This code is internal and subject to change without notice.
+ * createDisplay() for Firefox requires an options object with the following
+ * members:
+ * - contentDocument: From the window of the attached tab
+ * - chromeDocument: GCLITerm.document
+ * - environment.hudId: GCLITerm.hudId
+ * - jsEnvironment.globalObject: 'window'
+ * - jsEnvironment.evalFunction: 'eval' in a sandbox
+ * - inputElement: GCLITerm.inputNode
+ * - completeElement: GCLITerm.completeNode
+ * - hintElement: GCLITerm.hintNode
+ * - inputBackgroundElement: GCLITerm.inputStack
+ */
+exports.createDisplay = function(opts) {
+  var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay;
+  return new FFDisplay(opts);
+};
+
+var prefSvc = Components.classes['@mozilla.org/preferences-service;1']
+                        .getService(Components.interfaces.nsIPrefService);
+var prefBranch = prefSvc.getBranch(null)
+                        .QueryInterface(Components.interfaces.nsIPrefBranch2);
+
+exports.hiddenByChromePref = function() {
+  return !prefBranch.prefHasUserValue('devtools.chrome.enabled');
+};
+
+
+try {
+  var Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services;
+  var stringBundle = Services.strings.createBundle(
+          'chrome://browser/locale/devtools/gclicommands.properties');
 
   /**
    * Lookup a string in the GCLI string bundle
-   * @param name The name to lookup
-   * @return The looked up name
    */
-  aMozl10n.lookup = function(name) {
+  exports.lookup = function(name) {
     try {
       return stringBundle.GetStringFromName(name);
     }
     catch (ex) {
-      throw new Error("Failure in lookup('" + name + "')");
+      throw new Error('Failure in lookup(\'' + name + '\')');
     }
   };
 
   /**
    * Lookup a string in the GCLI string bundle
-   * @param name The name to lookup
-   * @param swaps An array of swaps. See stringBundle.formatStringFromName
-   * @return The looked up name
    */
-  aMozl10n.lookupFormat = function(name, swaps) {
+  exports.lookupFormat = function(name, swaps) {
     try {
       return stringBundle.formatStringFromName(name, swaps, swaps.length);
     }
     catch (ex) {
-      throw new Error("Failure in lookupFormat('" + name + "')");
+      throw new Error('Failure in lookupFormat(\'' + name + '\')');
     }
   };
-
-})(mozl10n);
-
-define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/selection', 'gcli/types/command', 'gcli/types/date', '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/types', '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/date').startup();
-  require('gcli/types/javascript').startup();
-  require('gcli/types/node').startup();
-  require('gcli/types/resource').startup();
-  require('gcli/types/setting').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);
-
-  // The API for use by command authors
-  exports.addCommand = require('gcli/canon').addCommand;
-  exports.removeCommand = require('gcli/canon').removeCommand;
-  exports.addConverter = require('gcli/converters').addConverter;
-  exports.removeConverter = require('gcli/converters').removeConverter;
-  exports.addType = require('gcli/types').addType;
-  exports.removeType = require('gcli/types').removeType;
-
-  exports.lookup = mozl10n.lookup;
-  exports.lookupFormat = mozl10n.lookupFormat;
-
-  /**
-   * This code is internal and subject to change without notice.
-   * createView() for Firefox requires an options object with the following
-   * members:
-   * - contentDocument: From the window of the attached tab
-   * - chromeDocument: GCLITerm.document
-   * - environment.hudId: GCLITerm.hudId
-   * - jsEnvironment.globalObject: 'window'
-   * - jsEnvironment.evalFunction: 'eval' in a sandbox
-   * - inputElement: GCLITerm.inputNode
-   * - completeElement: GCLITerm.completeNode
-   * - hintElement: GCLITerm.hintNode
-   * - inputBackgroundElement: GCLITerm.inputStack
-   */
-  exports.createDisplay = function(opts) {
-    var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay;
-    return new FFDisplay(opts);
-  };
-
-  exports.hiddenByChromePref = function() {
-    return !prefBranch.prefHasUserValue("devtools.chrome.enabled");
+}
+catch (ex) {
+  console.error('Using string fallbacks', ex);
+
+  exports.lookup = function(name) {
+    return name;
   };
-
-});
-/*
- * 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/types/basic', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) {
-
-'use strict';
-
-var promise = require('util/promise');
-var util = require('util/util');
-var l10n = require('util/l10n');
-var types = require('gcli/types');
-var Type = require('gcli/types').Type;
-var Status = require('gcli/types').Status;
-var Conversion = require('gcli/types').Conversion;
-var ArrayConversion = require('gcli/types').ArrayConversion;
-var SelectionType = require('gcli/types/selection').SelectionType;
-
-var BlankArgument = require('gcli/argument').BlankArgument;
-var ArrayArgument = require('gcli/argument').ArrayArgument;
-
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(StringType);
-  types.addType(NumberType);
-  types.addType(BooleanType);
-  types.addType(BlankType);
-  types.addType(DelegateType);
-  types.addType(ArrayType);
-};
-
-exports.shutdown = function() {
-  types.removeType(StringType);
-  types.removeType(NumberType);
-  types.removeType(BooleanType);
-  types.removeType(BlankType);
-  types.removeType(DelegateType);
-  types.removeType(ArrayType);
-};
-
-
-/**
- * 'string' the most basic string type where all we need to do is to take care
- * of converting escaped characters like \t, \n, etc. For the full list see
- * https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals
- *
- * The exception is that we ignore \b because replacing '\b' characters in
- * stringify() with their escaped version injects '\\b' all over the place and
- * the need to support \b seems low)
- *
- * @param typeSpec Options object, currently obeys only one parameter:
- * - allowBlank: Allow a blank string to be counted as valid
- */
-function StringType(typeSpec) {
-  this._allowBlank = !!typeSpec.allowBlank;
-}
-
-StringType.prototype = Object.create(Type.prototype);
-
-StringType.prototype.stringify = function(value, context) {
-  if (value == null) {
-    return '';
-  }
-
-  return value
-       .replace(/\\/g, '\\\\')
-       .replace(/\f/g, '\\f')
-       .replace(/\n/g, '\\n')
-       .replace(/\r/g, '\\r')
-       .replace(/\t/g, '\\t')
-       .replace(/\v/g, '\\v')
-       .replace(/\n/g, '\\n')
-       .replace(/\r/g, '\\r')
-       .replace(/ /g, '\\ ')
-       .replace(/'/g, '\\\'')
-       .replace(/"/g, '\\"')
-       .replace(/{/g, '\\{')
-       .replace(/}/g, '\\}');
-};
-
-StringType.prototype.parse = function(arg, context) {
-  if (!this._allowBlank && (arg.text == null || arg.text === '')) {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
-  }
-
-  // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it
-  // is double escaped)) is first converted to a private unicode character and
-  // then at the end from \uF000 to a single '\' to avoid the string \\n being
-  // converted first to \n and then to a <LF>
-
-  var value = arg.text
-       .replace(/\\\\/g, '\uF000')
-       .replace(/\\f/g, '\f')
-       .replace(/\\n/g, '\n')
-       .replace(/\\r/g, '\r')
-       .replace(/\\t/g, '\t')
-       .replace(/\\v/g, '\v')
-       .replace(/\\n/g, '\n')
-       .replace(/\\r/g, '\r')
-       .replace(/\\ /g, ' ')
-       .replace(/\\'/g, '\'')
-       .replace(/\\"/g, '"')
-       .replace(/\\{/g, '{')
-       .replace(/\\}/g, '}')
-       .replace(/\uF000/g, '\\');
-
-  return promise.resolve(new Conversion(value, arg));
-};
-
-StringType.prototype.name = 'string';
-
-exports.StringType = StringType;
-
-
-/**
- * We distinguish between integers and floats with the _allowFloat flag.
- */
-function NumberType(typeSpec) {
-  // Default to integer values
-  this._allowFloat = !!typeSpec.allowFloat;
-
-  if (typeSpec) {
-    this._min = typeSpec.min;
-    this._max = typeSpec.max;
-    this._step = typeSpec.step || 1;
-
-    if (!this._allowFloat &&
-        (this._isFloat(this._min) ||
-         this._isFloat(this._max) ||
-         this._isFloat(this._step))) {
-      throw new Error('allowFloat is false, but non-integer values given in type spec');
-    }
-  }
-  else {
-    this._step = 1;
-  }
-}
-
-NumberType.prototype = Object.create(Type.prototype);
-
-NumberType.prototype.stringify = function(value, context) {
-  if (value == null) {
-    return '';
-  }
-  return '' + value;
-};
-
-NumberType.prototype.getMin = function(context) {
-  if (this._min) {
-    if (typeof this._min === 'function') {
-      return this._min(context);
-    }
-    if (typeof this._min === 'number') {
-      return this._min;
-    }
-  }
-  return undefined;
-};
-
-NumberType.prototype.getMax = function(context) {
-  if (this._max) {
-    if (typeof this._max === 'function') {
-      return this._max(context);
-    }
-    if (typeof this._max === 'number') {
-      return this._max;
-    }
-  }
-  return undefined;
-};
-
-NumberType.prototype.parse = function(arg, context) {
-  if (arg.text.replace(/^\s*-?/, '').length === 0) {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
-  }
-
-  if (!this._allowFloat && (arg.text.indexOf('.') !== -1)) {
-    var message = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]);
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-  }
-
-  var value;
-  if (this._allowFloat) {
-    value = parseFloat(arg.text);
-  }
-  else {
-    value = parseInt(arg.text, 10);
-  }
-
-  if (isNaN(value)) {
-    var message = l10n.lookupFormat('typesNumberNan', [ arg.text ]);
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-  }
-
-  var max = this.getMax(context);
-  if (max != null && value > max) {
-    var message = l10n.lookupFormat('typesNumberMax', [ value, max ]);
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-  }
-
-  var min = this.getMin(context);
-  if (min != null && value < min) {
-    var message = l10n.lookupFormat('typesNumberMin', [ value, min ]);
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-  }
-
-  return promise.resolve(new Conversion(value, arg));
-};
-
-NumberType.prototype.decrement = function(value, context) {
-  if (typeof value !== 'number' || isNaN(value)) {
-    return this.getMax(context) || 1;
-  }
-  var newValue = value - this._step;
-  // Snap to the nearest incremental of the step
-  newValue = Math.ceil(newValue / this._step) * this._step;
-  return this._boundsCheck(newValue, context);
-};
-
-NumberType.prototype.increment = function(value, context) {
-  if (typeof value !== 'number' || isNaN(value)) {
-    var min = this.getMin(context);
-    return min != null ? min : 0;
-  }
-  var newValue = value + this._step;
-  // Snap to the nearest incremental of the step
-  newValue = Math.floor(newValue / this._step) * this._step;
-  if (this.getMax(context) == null) {
-    return newValue;
-  }
-  return this._boundsCheck(newValue, context);
-};
-
-/**
- * Return the input value so long as it is within the max/min bounds. If it is
- * lower than the minimum, return the minimum. If it is bigger than the maximum
- * then return the maximum.
- */
-NumberType.prototype._boundsCheck = function(value, context) {
-  var min = this.getMin(context);
-  if (min != null && value < min) {
-    return min;
-  }
-  var max = this.getMax(context);
-  if (max != null && value > max) {
-    return max;
-  }
-  return value;
-};
-
-/**
- * Return true if the given value is a finite number and not an integer, else
- * return false.
- */
-NumberType.prototype._isFloat = function(value) {
-  return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0));
-};
-
-NumberType.prototype.name = 'number';
-
-exports.NumberType = NumberType;
-
-
-/**
- * true/false values
- */
-function BooleanType(typeSpec) {
-}
-
-BooleanType.prototype = Object.create(SelectionType.prototype);
-
-BooleanType.prototype.lookup = [
-  { name: 'false', value: false },
-  { name: 'true', value: true }
-];
-
-BooleanType.prototype.parse = function(arg, context) {
-  if (arg.type === 'TrueNamedArgument') {
-    return promise.resolve(new Conversion(true, arg));
-  }
-  if (arg.type === 'FalseNamedArgument') {
-    return promise.resolve(new Conversion(false, arg));
-  }
-  return SelectionType.prototype.parse.call(this, arg, context);
-};
-
-BooleanType.prototype.stringify = function(value, context) {
-  if (value == null) {
-    return '';
-  }
-  return '' + value;
-};
-
-BooleanType.prototype.getBlank = function(context) {
-  return new Conversion(false, new BlankArgument(), Status.VALID, '',
-                        promise.resolve(this.lookup));
-};
-
-BooleanType.prototype.name = 'boolean';
-
-exports.BooleanType = BooleanType;
-
-
-/**
- * A type for "we don't know right now, but hope to soon".
- */
-function DelegateType(typeSpec) {
-  if (typeof typeSpec.delegateType !== 'function') {
-    throw new Error('Instances of DelegateType need typeSpec.delegateType to be a function that returns a type');
-  }
-  Object.keys(typeSpec).forEach(function(key) {
-    this[key] = typeSpec[key];
-  }, this);
-}
-
-/**
- * Child types should implement this method to return an instance of the type
- * that should be used. If no type is available, or some sort of temporary
- * placeholder is required, BlankType can be used.
- * @param context An ExecutionContext to allow access to information about the
- * current state of the command line, or null if one is not available. A
- * context is available for stringify, parse*, [in|de]crement and getType
- * functions but not for isImportant
- */
-DelegateType.prototype.delegateType = function(context) {
-  throw new Error('Not implemented');
-};
-
-DelegateType.prototype = Object.create(Type.prototype);
-
-DelegateType.prototype.stringify = function(value, context) {
-  return this.delegateType(context).stringify(value, context);
-};
-
-DelegateType.prototype.parse = function(arg, context) {
-  return this.delegateType(context).parse(arg, context);
-};
-
-DelegateType.prototype.decrement = function(value, context) {
-  var delegated = this.delegateType(context);
-  return (delegated.decrement ? delegated.decrement(value, context) : undefined);
-};
-
-DelegateType.prototype.increment = function(value, context) {
-  var delegated = this.delegateType(context);
-  return (delegated.increment ? delegated.increment(value, context) : undefined);
-};
-
-DelegateType.prototype.getType = function(context) {
-  return this.delegateType(context);
-};
-
-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.
- */
-function BlankType(typeSpec) {
-}
-
-BlankType.prototype = Object.create(Type.prototype);
-
-BlankType.prototype.stringify = function(value, context) {
-  return '';
-};
-
-BlankType.prototype.parse = function(arg, context) {
-  return promise.resolve(new Conversion(undefined, arg));
-};
-
-BlankType.prototype.name = 'blank';
-
-exports.BlankType = BlankType;
-
-
-/**
- * A set of objects of the same type
- */
-function ArrayType(typeSpec) {
-  if (!typeSpec.subtype) {
-    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.createType(this.subtype);
-}
-
-ArrayType.prototype = Object.create(Type.prototype);
-
-ArrayType.prototype.stringify = function(values, context) {
-  if (values == null) {
-    return '';
-  }
-  // BUG 664204: Check for strings with spaces and add quotes
-  return values.join(' ');
-};
-
-ArrayType.prototype.parse = function(arg, context) {
-  if (arg.type !== 'ArrayArgument') {
-    console.error('non ArrayArgument to ArrayType.parse', arg);
-    throw new Error('non ArrayArgument to ArrayType.parse');
-  }
-
-  // Parse an argument to a conversion
-  // Hack alert. ArrayConversion needs to be able to answer questions about
-  // the status of individual conversions in addition to the overall state.
-  // |subArg.conversion| allows us to do that easily.
-  var subArgParse = function(subArg) {
-    return this.subtype.parse(subArg, context).then(function(conversion) {
-      subArg.conversion = conversion;
-      return conversion;
-    }.bind(this));
-  }.bind(this);
-
-  var conversionPromises = arg.getArguments().map(subArgParse);
-  return promise.all(conversionPromises).then(function(conversions) {
-    return new ArrayConversion(conversions, arg);
-  });
-};
-
-ArrayType.prototype.getBlank = function(context) {
-  return new ArrayConversion([], new ArrayArgument());
-};
-
-ArrayType.prototype.name = 'array';
-
-exports.ArrayType = ArrayType;
+  exports.lookupFormat = function(name, swaps) {
+    return name;
+  };
+}
 
 
 });
 /*
  * 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.
@@ -650,29 +178,299 @@ exports.ArrayType = ArrayType;
  *
  * 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/promise', ['require', 'exports', 'module' ], function(require, exports, module) {
+define('gcli/settings', ['require', 'exports', 'module' , 'util/util', 'gcli/types'], function(require, exports, module) {
 
 'use strict';
 
-var imported = {};
-Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js",
-                        imported);
-
-exports.defer = imported.Promise.defer;
-exports.resolve = imported.Promise.resolve;
-exports.reject = imported.Promise.reject;
-exports.promised = imported.Promise.promised;
-exports.all = imported.Promise.all;
+var imports = {};
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports);
+
+imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() {
+  var prefService = Components.classes['@mozilla.org/preferences-service;1']
+          .getService(Components.interfaces.nsIPrefService);
+  return prefService.getBranch(null)
+          .QueryInterface(Components.interfaces.nsIPrefBranch2);
+});
+
+imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() {
+  return Components.classes["@mozilla.org/supports-string;1"]
+          .createInstance(Components.interfaces.nsISupportsString);
+});
+
+
+var util = require('util/util');
+var types = require('gcli/types');
+
+/**
+ * All local settings have this prefix when used in Firefox
+ */
+var DEVTOOLS_PREFIX = 'devtools.gcli.';
+
+/**
+ * A class to wrap up the properties of a preference.
+ * @see toolkit/components/viewconfig/content/config.js
+ */
+function Setting(prefSpec) {
+  if (typeof prefSpec === 'string') {
+    // We're coming from getAll() i.e. a full listing of prefs
+    this.name = prefSpec;
+    this.description = '';
+  }
+  else {
+    // A specific addition by GCLI
+    this.name = DEVTOOLS_PREFIX + prefSpec.name;
+
+    if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) {
+      if (this.type.name !== prefSpec.type) {
+        throw new Error('Locally declared type (' + prefSpec.type + ') != ' +
+            'Mozilla declared type (' + this.type.name + ') for ' + this.name);
+      }
+    }
+
+    this.description = prefSpec.description;
+  }
+
+  this.onChange = util.createEvent('Setting.onChange');
+}
+
+/**
+ * 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.createType('boolean');
+
+      case imports.prefBranch.PREF_INT:
+        return types.createType('number');
+
+      case imports.prefBranch.PREF_STRING:
+        return types.createType('string');
+
+      default:
+        throw new Error('Unknown type for ' + this.name);
+    }
+  },
+  enumerable: true
+});
+
+/**
+ * What type is this property: boolean/integer/string?
+ */
+Object.defineProperty(Setting.prototype, 'value', {
+  get: function() {
+    switch (imports.prefBranch.getPrefType(this.name)) {
+      case imports.prefBranch.PREF_BOOL:
+        return imports.prefBranch.getBoolPref(this.name);
+
+      case imports.prefBranch.PREF_INT:
+        return imports.prefBranch.getIntPref(this.name);
+
+      case imports.prefBranch.PREF_STRING:
+        var value = imports.prefBranch.getComplexValue(this.name,
+                Components.interfaces.nsISupportsString).data;
+        // In case of a localized string
+        if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) {
+          value = imports.prefBranch.getComplexValue(this.name,
+                  Components.interfaces.nsIPrefLocalizedString).data;
+        }
+        return value;
+
+      default:
+        throw new Error('Invalid value for ' + this.name);
+    }
+  },
+
+  set: function(value) {
+    if (imports.prefBranch.prefIsLocked(this.name)) {
+      throw new Error('Locked preference ' + this.name);
+    }
+
+    switch (imports.prefBranch.getPrefType(this.name)) {
+      case imports.prefBranch.PREF_BOOL:
+        imports.prefBranch.setBoolPref(this.name, value);
+        break;
+
+      case imports.prefBranch.PREF_INT:
+        imports.prefBranch.setIntPref(this.name, value);
+        break;
+
+      case imports.prefBranch.PREF_STRING:
+        imports.supportsString.data = value;
+        imports.prefBranch.setComplexValue(this.name,
+                Components.interfaces.nsISupportsString,
+                imports.supportsString);
+        break;
+
+      default:
+        throw new Error('Invalid value for ' + this.name);
+    }
+
+    Services.prefs.savePrefFile(null);
+  },
+
+  enumerable: true
+});
+
+/**
+ * Reset this setting to it's initial default value
+ */
+Setting.prototype.setDefault = function() {
+  imports.prefBranch.clearUserPref(this.name);
+  Services.prefs.savePrefFile(null);
+};
+
+
+/**
+ * Collection of preferences for sorted access
+ */
+var settingsAll = [];
+
+/**
+ * Collection of preferences for fast indexed access
+ */
+var settingsMap = new Map();
+
+/**
+ * Flag so we know if we've read the system preferences
+ */
+var hasReadSystem = false;
+
+/**
+ * Clear out all preferences and return to initial state
+ */
+function reset() {
+  settingsMap = new Map();
+  settingsAll = [];
+  hasReadSystem = false;
+}
+
+/**
+ * Reset everything on startup and shutdown because we're doing lazy loading
+ */
+exports.startup = function() {
+  reset();
+};
+
+exports.shutdown = function() {
+  reset();
+};
+
+/**
+ * Load system prefs if they've not been loaded already
+ * @return true
+ */
+function readSystem() {
+  if (hasReadSystem) {
+    return;
+  }
+
+  imports.prefBranch.getChildList('').forEach(function(name) {
+    var setting = new Setting(name);
+    settingsAll.push(setting);
+    settingsMap.set(name, setting);
+  });
+
+  settingsAll.sort(function(s1, s2) {
+    return s1.name.localeCompare(s2.name);
+  });
+
+  hasReadSystem = true;
+}
+
+/**
+ * Get an array containing all known Settings filtered to match the given
+ * filter (string) at any point in the name of the setting
+ */
+exports.getAll = function(filter) {
+  readSystem();
+
+  if (filter == null) {
+    return settingsAll;
+  }
+
+  return settingsAll.filter(function(setting) {
+    return setting.name.indexOf(filter) !== -1;
+  });
+};
+
+/**
+ * Add a new setting.
+ */
+exports.addSetting = function(prefSpec) {
+  var setting = new Setting(prefSpec);
+
+  if (settingsMap.has(setting.name)) {
+    // Once exists already, we're going to need to replace it in the array
+    for (var i = 0; i < settingsAll.length; i++) {
+      if (settingsAll[i].name === setting.name) {
+        settingsAll[i] = setting;
+      }
+    }
+  }
+
+  settingsMap.set(setting.name, setting);
+  exports.onChange({ added: setting.name });
+
+  return setting;
+};
+
+/**
+ * Getter for an existing setting. Generally use of this function should be
+ * avoided. Systems that define a setting should export it if they wish it to
+ * be available to the outside, or not otherwise. Use of this function breaks
+ * that boundary and also hides dependencies. Acceptable uses include testing
+ * and embedded uses of GCLI that pre-define all settings (e.g. Firefox)
+ * @param name The name of the setting to fetch
+ * @return The found Setting object, or undefined if the setting was not found
+ */
+exports.getSetting = function(name) {
+  // We might be able to give the answer without needing to read all system
+  // settings if this is an internal setting
+  var found = settingsMap.get(name);
+  if (!found) {
+    found = settingsMap.get(DEVTOOLS_PREFIX + name);
+  }
+
+  if (found) {
+    return found;
+  }
+
+  if (hasReadSystem) {
+    return undefined;
+  }
+  else {
+    readSystem();
+    var found = settingsMap.get(name);
+    if (!found) {
+      found = settingsMap.get(DEVTOOLS_PREFIX + name);
+    }
+    return found;
+  }
+};
+
+/**
+ * Event for use to detect when the list of settings changes
+ */
+exports.onChange = util.createEvent('Settings.onChange');
+
+/**
+ * Remove a setting. A no-op in this case
+ */
+exports.removeSetting = function() { };
+
 
 });
 /*
  * 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
@@ -908,31 +706,31 @@ exports.createEvent = function(name) {
 
 var promise = require('util/promise');
 
 /**
  * Utility to convert a resolved promise to a concrete value.
  * Warning: This is something of an experiment. The alternative of mixing
  * concrete/promise return values could be better.
  */
-exports.synchronize = function(promise) {
-  if (promise == null || typeof promise.then !== 'function') {
-    return promise;
+exports.synchronize = function(p) {
+  if (p == null || typeof p.then !== 'function') {
+    return p;
   }
   var failure = undefined;
   var reply = undefined;
   var onDone = function(value) {
     failure = false;
     reply = value;
   };
   var onError = function (value) {
     failure = true;
     reply = value;
   };
-  promise.then(onDone, onError);
+  p.then(onDone, onError);
   if (failure === undefined) {
     throw new Error('non synchronizable promise');
   }
   if (failure) {
     throw reply;
   }
   return reply;
 };
@@ -1478,90 +1276,29 @@ else {
  *
  * 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/l10n', ['require', 'exports', 'module' ], function(require, exports, module) {
+define('util/promise', ['require', 'exports', 'module' ], function(require, exports, module) {
 
 'use strict';
 
-Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
-Components.utils.import('resource://gre/modules/Services.jsm');
-
-var imports = {};
-XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () {
-  return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties');
-});
-
-/*
- * Not supported when embedded - we're doing things the Mozilla way not the
- * require.js way.
- */
-exports.registerStringsSource = function(modulePath) {
-  throw new Error('registerStringsSource is not available in mozilla');
-};
-
-exports.unregisterStringsSource = function(modulePath) {
-  throw new Error('unregisterStringsSource is not available in mozilla');
-};
-
-exports.lookupSwap = function(key, swaps) {
-  throw new Error('lookupSwap is not available in mozilla');
-};
-
-exports.lookupPlural = function(key, ord, swaps) {
-  throw new Error('lookupPlural is not available in mozilla');
-};
-
-exports.getPreferredLocales = function() {
-  return [ 'root' ];
-};
-
-/** @see lookup() in lib/util/l10n.js */
-exports.lookup = function(key) {
-  try {
-    // Our memory leak hunter walks reachable objects trying to work out what
-    // type of thing they are using object.constructor.name. If that causes
-    // problems then we can avoid the unknown-key-exception with the following:
-    /*
-    if (key === 'constructor') {
-      return { name: 'l10n-mem-leak-defeat' };
-    }
-    */
-
-    return imports.stringBundle.GetStringFromName(key);
-  }
-  catch (ex) {
-    console.error('Failed to lookup ', key, ex);
-    return key;
-  }
-};
-
-/** @see propertyLookup in lib/util/l10n.js */
-exports.propertyLookup = Proxy.create({
-  get: function(rcvr, name) {
-    return exports.lookup(name);
-  }
-});
-
-/** @see lookupFormat in lib/util/l10n.js */
-exports.lookupFormat = function(key, swaps) {
-  try {
-    return imports.stringBundle.formatStringFromName(key, swaps, swaps.length);
-  }
-  catch (ex) {
-    console.error('Failed to format ', key, ex);
-    return key;
-  }
-};
-
+var imported = {};
+Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js",
+                        imported);
+
+exports.defer = imported.Promise.defer;
+exports.resolve = imported.Promise.resolve;
+exports.reject = imported.Promise.reject;
+exports.promised = imported.Promise.promised;
+exports.all = imported.Promise.all;
 
 });
 /*
  * 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
@@ -1634,16 +1371,29 @@ var Status = {
       if (Array.isArray(status)) {
         status = Status.combine.apply(null, status);
       }
       if (status > combined) {
         combined = status;
       }
     }
     return combined;
+  },
+
+  fromString: function(str) {
+    switch (str) {
+      case Status.VALID.toString():
+        return Status.VALID;
+      case Status.INCOMPLETE.toString():
+        return Status.INCOMPLETE;
+      case Status.ERROR.toString():
+        return Status.ERROR;
+      default:
+        throw new Error('\'' + str + '\' is not a status');
+    }
   }
 };
 
 exports.Status = Status;
 
 
 /**
  * The type.parse() method converts an Argument into a value, Conversion is
@@ -1705,19 +1455,21 @@ function Conversion(value, arg, status, 
 }
 
 /**
  * Ensure that all arguments that are part of this conversion know what they
  * are assigned to.
  * @param assignment The Assignment (param/conversion link) to inform the
  * argument about.
  */
-Conversion.prototype.assign = function(assignment) {
-  this.arg.assign(assignment);
-};
+Object.defineProperty(Conversion.prototype, 'assignment', {
+  get: function() { return this.arg.assignment; },
+  set: function(assignment) { this.arg.assignment = assignment; },
+  enumerable: true
+});
 
 /**
  * Work out if there is information provided in the contained argument.
  */
 Conversion.prototype.isDataProvided = function() {
   return this.arg.type !== 'BlankArgument';
 };
 
@@ -1843,22 +1595,27 @@ function ArrayConversion(conversions, ar
   this.message = '';
 
   // Predictions are generally provided by individual values
   this.predictions = [];
 }
 
 ArrayConversion.prototype = Object.create(Conversion.prototype);
 
-ArrayConversion.prototype.assign = function(assignment) {
-  this.conversions.forEach(function(conversion) {
-    conversion.assign(assignment);
-  }, this);
-  this.assignment = assignment;
-};
+Object.defineProperty(ArrayConversion.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) {
+    this._assignment = assignment;
+
+    this.conversions.forEach(function(conversion) {
+      conversion.assignment = assignment;
+    }, this);
+  },
+  enumerable: true
+});
 
 ArrayConversion.prototype.getStatus = function(arg) {
   if (arg && arg.conversion) {
     return arg.conversion.getStatus();
   }
   return this._status;
 };
 
@@ -1977,16 +1734,22 @@ Type.prototype.getBlank = function(conte
  * be able to lie about it's type for fields to accept it as one of their own.
  * Sub-types can ignore this unless they're DelegateType.
  * @param context An ExecutionContext to allow basic Requisition access
  */
 Type.prototype.getType = function(context) {
   return this;
 };
 
+/**
+ * addItems allows registrations of a number of things. This allows it to know
+ * what type of item, and how it should be registered.
+ */
+Type.prototype.item = 'type';
+
 exports.Type = Type;
 
 /**
  * Private registry of types
  * Invariant: types[name] = type.name
  */
 var registeredTypes = {};
 
@@ -2008,19 +1771,16 @@ exports.addType = function(type) {
     if (!type.name) {
       throw new Error('All registered types must have a name');
     }
 
     if (type instanceof Type) {
       registeredTypes[type.name] = type;
     }
     else {
-      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;
@@ -2052,51 +1812,81 @@ exports.createType = function(typeSpec) 
   if (typeof typeSpec === 'string') {
     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];
+  var type, newType;
+  if (typeSpec.name == null || typeSpec.name == 'type') {
+    type = Type;
+  }
+  else {
+    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'
+    // 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];
-    }
-
+    copyProperties(type, newType);
+  }
+
+  // Copy the properties of typeSpec onto the new type
+  copyProperties(typeSpec, newType);
+
+  if (typeof type !== 'function') {
     if (typeof newType.constructor === 'function') {
       newType.constructor();
     }
   }
 
   return newType;
 };
 
+function copyProperties(src, dest) {
+  for (var key in src) {
+    var descriptor;
+    var obj = src;
+    while (true) {
+      descriptor = Object.getOwnPropertyDescriptor(obj, key);
+      if (descriptor != null) {
+        break;
+      }
+      obj = Object.getPrototypeOf(obj);
+      if (obj == null) {
+        throw new Error('Can\'t find descriptor of ' + key);
+      }
+    }
+
+    if ('value' in descriptor) {
+      dest[key] = src[key];
+    }
+    else if ('get' in descriptor) {
+      Object.defineProperty(dest, key, {
+        get: descriptor.get,
+        set: descriptor.set,
+        enumerable: descriptor.enumerable
+      });
+    }
+    else {
+      throw new Error('Don\'t know how to copy ' + key + ' property.');
+    }
+  }
+}
+
 
 });
 /*
  * 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
@@ -2216,19 +2006,21 @@ Argument.prototype.beget = function(opti
 
   var type = options.type || Argument;
   return new type(text, prefix, suffix);
 };
 
 /**
  * We need to keep track of which assignment we've been assigned to
  */
-Argument.prototype.assign = function(assignment) {
-  this.assignment = assignment;
-};
+Object.defineProperty(Argument.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) { this._assignment = assignment; },
+  enumerable: true
+});
 
 /**
  * Sub-classes of Argument are collections of arguments, getArgs() gets access
  * to the members of the collection in order to do things like re-create input
  * command lines. For the simple Argument case it's just an array containing
  * only this.
  */
 Argument.prototype.getArgs = function() {
@@ -2396,23 +2188,27 @@ function MergedArgument(args, start, end
 MergedArgument.prototype = Object.create(Argument.prototype);
 
 MergedArgument.prototype.type = 'MergedArgument';
 
 /**
  * Keep track of which assignment we've been assigned to, and allow the
  * original args to do the same.
  */
-MergedArgument.prototype.assign = function(assignment) {
-  this.args.forEach(function(arg) {
-    arg.assign(assignment);
-  }, this);
-
-  this.assignment = assignment;
-};
+Object.defineProperty(MergedArgument.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) {
+    this._assignment = assignment;
+
+    this.args.forEach(function(arg) {
+      arg.assignment = assignment;
+    }, this);
+  },
+  enumerable: true
+});
 
 MergedArgument.prototype.getArgs = function() {
   return this.args;
 };
 
 MergedArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
@@ -2440,22 +2236,27 @@ function TrueNamedArgument(arg) {
   this.prefix = arg.prefix;
   this.suffix = arg.suffix;
 }
 
 TrueNamedArgument.prototype = Object.create(Argument.prototype);
 
 TrueNamedArgument.prototype.type = 'TrueNamedArgument';
 
-TrueNamedArgument.prototype.assign = function(assignment) {
-  if (this.arg) {
-    this.arg.assign(assignment);
-  }
-  this.assignment = assignment;
-};
+Object.defineProperty(TrueNamedArgument.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) {
+    this._assignment = assignment;
+
+    if (this.arg) {
+      this.arg.assignment = assignment;
+    }
+  },
+  enumerable: true
+});
 
 TrueNamedArgument.prototype.getArgs = function() {
   return [ this.arg ];
 };
 
 TrueNamedArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
@@ -2557,23 +2358,28 @@ function NamedArgument() {
     this.suffix = this.valueArg.suffix;
   }
 }
 
 NamedArgument.prototype = Object.create(Argument.prototype);
 
 NamedArgument.prototype.type = 'NamedArgument';
 
-NamedArgument.prototype.assign = function(assignment) {
-  this.nameArg.assign(assignment);
-  if (this.valueArg != null) {
-    this.valueArg.assign(assignment);
-  }
-  this.assignment = assignment;
-};
+Object.defineProperty(NamedArgument.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) {
+    this._assignment = assignment;
+
+    this.nameArg.assignment = assignment;
+    if (this.valueArg != null) {
+      this.valueArg.assignment = assignment;
+    }
+  },
+  enumerable: true
+});
 
 NamedArgument.prototype.getArgs = function() {
   return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ];
 };
 
 NamedArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
@@ -2637,23 +2443,27 @@ ArrayArgument.prototype.addArgument = fu
 ArrayArgument.prototype.addArguments = function(args) {
   Array.prototype.push.apply(this.args, args);
 };
 
 ArrayArgument.prototype.getArguments = function() {
   return this.args;
 };
 
-ArrayArgument.prototype.assign = function(assignment) {
-  this.args.forEach(function(arg) {
-    arg.assign(assignment);
-  }, this);
-
-  this.assignment = assignment;
-};
+Object.defineProperty(ArrayArgument.prototype, 'assignment', {
+  get: function() { return this._assignment; },
+  set: function(assignment) {
+    this._assignment = assignment;
+
+    this.args.forEach(function(arg) {
+      arg.assignment = assignment;
+    }, this);
+  },
+  enumerable: true
+});
 
 ArrayArgument.prototype.getArgs = function() {
   return this.args;
 };
 
 ArrayArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
@@ -2703,44 +2513,1247 @@ exports.ArrayArgument = ArrayArgument;
  *
  * 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/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types', 'gcli/types/spell', 'gcli/argument'], function(require, exports, module) {
+define('gcli/api', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/converters', 'gcli/types', 'gcli/settings', 'gcli/ui/fields'], function(require, exports, module) {
+
+'use strict';
+
+var canon = require('gcli/canon');
+var converters = require('gcli/converters');
+var types = require('gcli/types');
+var settings = require('gcli/settings');
+var fields = require('gcli/ui/fields');
+
+/**
+ * This is the heart of the API that we expose to the outside
+ */
+exports.getApi = function() {
+  return {
+    addCommand: canon.addCommand,
+    removeCommand: canon.removeCommand,
+    addConverter: converters.addConverter,
+    removeConverter: converters.removeConverter,
+    addType: types.addType,
+    removeType: types.removeType,
+
+    addItems: function(items) {
+      items.forEach(function(item) {
+        // Some items are registered using the constructor so we need to check
+        // the prototype for the the type of the item
+        var type = item.item;
+        if (type == null && item.prototype) {
+            type = item.prototype.item;
+        }
+        if (type === 'command') {
+          canon.addCommand(item);
+        }
+        else if (type === 'type') {
+          types.addType(item);
+        }
+        else if (type === 'converter') {
+          converters.addConverter(item);
+        }
+        else if (type === 'setting') {
+          settings.addSetting(item);
+        }
+        else if (type === 'field') {
+          fields.addField(item);
+        }
+        else {
+          console.error('Error for: ', item);
+          throw new Error('item property not found');
+        }
+      });
+    },
+
+    removeItems: function(items) {
+      items.forEach(function(item) {
+        if (item.item === 'command') {
+          canon.removeCommand(item);
+        }
+        else if (item.item === 'type') {
+          types.removeType(item);
+        }
+        else if (item.item === 'converter') {
+          converters.removeConverter(item);
+        }
+        else if (item.item === 'settings') {
+          settings.removeSetting(item);
+        }
+        else if (item.item === 'field') {
+          fields.removeField(item);
+        }
+        else {
+          throw new Error('item property not found');
+        }
+      });
+    }
+  };
+};
+
+/**
+ * api.getApi() is clean, but generally we want to add the functions to the
+ * 'exports' object. So this is a quick helper.
+ */
+exports.populateApi = function(obj) {
+  var exportable = exports.getApi();
+  Object.keys(exportable).forEach(function(key) {
+    obj[key] = exportable[key];
+  });
+};
+
+});
+/*
+ * 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/canon', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+var util = require('util/util');
+var l10n = require('util/l10n');
+
+var types = require('gcli/types');
+var Status = require('gcli/types').Status;
+
+/**
+ * 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.
+ */
+function lookup(data, onUndefined) {
+  if (data == null) {
+    if (onUndefined) {
+      return l10n.lookup(onUndefined);
+    }
+
+    return data;
+  }
+
+  if (typeof data === 'string') {
+    return data;
+  }
+
+  if (typeof data === 'object') {
+    if (data.key) {
+      return l10n.lookup(data.key);
+    }
+
+    var locales = l10n.getPreferredLocales();
+    var translated;
+    locales.some(function(locale) {
+      translated = data[locale];
+      return translated != null;
+    });
+    if (translated != null) {
+      return translated;
+    }
+
+    console.error('Can\'t find locale in descriptions: ' +
+            'locales=' + JSON.stringify(locales) + ', ' +
+            'description=' + JSON.stringify(data));
+    return '(No description)';
+  }
+
+  return l10n.lookup(onUndefined);
+}
+
+
+/**
+ * The command object is mostly just setup around a commandSpec (as passed to
+ * #addCommand()).
+ */
+function Command(commandSpec) {
+  Object.keys(commandSpec).forEach(function(key) {
+    this[key] = commandSpec[key];
+  }, this);
+
+  if (!this.name) {
+    throw new Error('All registered commands must have a name');
+  }
+
+  if (this.params == null) {
+    this.params = [];
+  }
+  if (!Array.isArray(this.params)) {
+    throw new Error('command.params must be an array in ' + this.name);
+  }
+
+  this.hasNamedParameters = false;
+  this.description = 'description' in this ? this.description : undefined;
+  this.description = lookup(this.description, 'canonDescNone');
+  this.manual = 'manual' in this ? this.manual : undefined;
+  this.manual = lookup(this.manual);
+
+  // At this point this.params has nested param groups. We want to flatten it
+  // out and replace the param object literals with Parameter objects
+  var paramSpecs = this.params;
+  this.params = [];
+
+  // Track if the user is trying to mix default params and param groups.
+  // All the non-grouped parameters must come before all the param groups
+  // because non-grouped parameters can be assigned positionally, so their
+  // index is important. We don't want 'holes' in the order caused by
+  // parameter groups.
+  var usingGroups = false;
+
+  // In theory this could easily be made recursive, so param groups could
+  // contain nested param groups. Current thinking is that the added
+  // complexity for the UI probably isn't worth it, so this implementation
+  // prevents nesting.
+  paramSpecs.forEach(function(spec) {
+    if (!spec.group) {
+      var param = new Parameter(spec, this, null);
+      this.params.push(param);
+
+      if (!param.isPositionalAllowed) {
+        this.hasNamedParameters = true;
+      }
+
+      if (usingGroups && param.groupName == null) {
+        throw new Error('Parameters can\'t come after param groups.' +
+                        ' Ignoring ' + this.name + '/' + spec.name);
+      }
+
+      if (param.groupName != null) {
+        usingGroups = true;
+      }
+    }
+    else {
+      spec.params.forEach(function(ispec) {
+        var param = new Parameter(ispec, this, spec.group);
+        this.params.push(param);
+
+        if (!param.isPositionalAllowed) {
+          this.hasNamedParameters = true;
+        }
+      }, this);
+
+      usingGroups = true;
+    }
+  }, this);
+}
+
+/**
+ * JSON serializer that avoids non-serializable data
+ */
+Object.defineProperty(Command.prototype, 'json', {
+  get: function() {
+    return {
+      name: this.name,
+      description: this.description,
+      manual: this.manual,
+      params: this.params.map(function(param) { return param.json; }),
+      returnType: this.returnType,
+      isParent: (this.exec == null)
+    };
+  },
+  enumerable: true
+});
+
+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' };
+  this.paramSpec = paramSpec;
+  this.name = this.paramSpec.name;
+  this.type = this.paramSpec.type;
+
+  this.groupName = groupName;
+  if (this.groupName != null) {
+    if (this.paramSpec.option != null) {
+      throw new Error('Can\'t have a "option" property in a nested parameter');
+    }
+  }
+  else {
+    if (this.paramSpec.option != null) {
+      this.groupName = this.paramSpec.option === true ?
+              l10n.lookup('canonDefaultGroupName') :
+              '' + this.paramSpec.option;
+    }
+  }
+
+  if (!this.name) {
+    throw new Error('In ' + this.command.name +
+                    ': all params must have a name');
+  }
+
+  var typeSpec = this.type;
+  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.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
+  // there is no defaultValue, and null is used when the parameter is
+  // optional, neither are required to parse and stringify.
+  if (this._defaultValue != null) {
+    try {
+      // Passing null in for a context is bound to get us into trouble some day
+      // in which case we'll need to mock one up in some way
+      var context = null;
+      var defaultText = this.type.stringify(this.paramSpec.defaultValue, context);
+      var parsed = this.type.parseString(defaultText, context);
+      parsed.then(function(defaultConversion) {
+        if (defaultConversion.getStatus() !== Status.VALID) {
+          console.error('In ' + this.command.name + '/' + this.name +
+                        ': Error round tripping defaultValue. status = ' +
+                        defaultConversion.getStatus());
+        }
+      }.bind(this), util.errorHandler);
+    }
+    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.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
+ */
+Object.defineProperty(Parameter.prototype, 'defaultValue', {
+  get: function() {
+    if (!('_defaultValue' in this)) {
+      this._defaultValue = (this.paramSpec.defaultValue !== undefined) ?
+          this.paramSpec.defaultValue :
+          this.type.getBlank().value;
+    }
+
+    return this._defaultValue;
+  },
+  enumerable : true
+});
+
+/**
+ * Does the given name uniquely identify this param (among the other params
+ * in this command)
+ * @param name The name to check
+ */
+Parameter.prototype.isKnownAs = function(name) {
+  if (name === '--' + this.name) {
+    return true;
+  }
+  return false;
+};
+
+/**
+ * Resolve the manual for this parameter, by looking in the paramSpec
+ * and doing a l10n lookup
+ */
+Object.defineProperty(Parameter.prototype, 'manual', {
+  get: function() {
+    return lookup(this.paramSpec.manual || undefined);
+  },
+  enumerable: true
+});
+
+/**
+ * Resolve the description for this parameter, by looking in the paramSpec
+ * and doing a l10n lookup
+ */
+Object.defineProperty(Parameter.prototype, 'description', {
+  get: function() {
+    return lookup(this.paramSpec.description || undefined, 'canonDescNone');
+  },
+  enumerable: true
+});
+
+/**
+ * Is the user required to enter data for this parameter? (i.e. has
+ * defaultValue been set to something other than undefined)
+ */
+Object.defineProperty(Parameter.prototype, 'isDataRequired', {
+  get: function() {
+    return this.defaultValue === undefined;
+  },
+  enumerable: true
+});
+
+/**
+ * Reflect the paramSpec 'hidden' property (dynamically so it can change)
+ */
+Object.defineProperty(Parameter.prototype, 'hidden', {
+  get: function() {
+    return this.paramSpec.hidden;
+  },
+  enumerable: true
+});
+
+/**
+ * Are we allowed to assign data to this parameter using positional
+ * parameters?
+ */
+Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', {
+  get: function() {
+    return this.groupName == null;
+  },
+  enumerable: true
+});
+
+/**
+ * JSON serializer that avoids non-serializable data
+ */
+Object.defineProperty(Parameter.prototype, 'json', {
+  get: function() {
+    var json = {
+      name: this.name,
+      type: this.paramSpec.type,
+      description: this.description
+    };
+    if (this.defaultValue !== undefined && json.type !== 'boolean') {
+      json.defaultValue = this.defaultValue;
+    }
+    if (this.option !== undefined) {
+      json.option = this.option;
+    }
+    return json;
+  },
+  enumerable: true
+});
+
+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.prototype.addCommand = function(commandSpec) {
+  if (this._commands[commandSpec.name] != null) {
+    // Roughly canon.removeCommand() without the event call, which we do later
+    delete this._commands[commandSpec.name];
+    this._commandNames = this._commandNames.filter(function(test) {
+      return test !== commandSpec.name;
+    });
+  }
+
+  var command = new Command(commandSpec);
+  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.prototype.removeCommand = function(commandOrName) {
+  var name = typeof commandOrName === 'string' ?
+          commandOrName :
+          commandOrName.name;
+
+  if (!this._commands[name]) {
+    return false;
+  }
+
+  // See start of canon.addCommand if changing this code
+  delete this._commands[name];
+  delete this._commandSpecs[name];
+  this._commandNames = this._commandNames.filter(function(test) {
+    return test !== name;
+  });
+
+  this.onCanonChange();
+  return true;
+};
+
+/**
+ * Retrieve a command by name
+ * @param name The name of the command to retrieve
+ */
+Canon.prototype.getCommand = function(name) {
+  // '|| undefined' is to silence 'reference to undefined property' warnings
+  return this._commands[name] || undefined;
+};
+
+/**
+ * Get an array of all the registered commands.
+ */
+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.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.prototype.getCommandSpecs = function() {
+  var specs = {};
+
+  Object.keys(this._commands).forEach(function(name) {
+    var command = this._commands[name];
+    if (!command.noRemote) {
+      specs[name] = command.json;
+    }
+  }.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.noRemote) {
+      return;
+    }
+
+    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');
+}
+
+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.
+ * 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/l10n', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+'use strict';
+
+Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
+Components.utils.import('resource://gre/modules/Services.jsm');
+
+var imports = {};
+XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () {
+  return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties');
+});
+
+/*
+ * Not supported when embedded - we're doing things the Mozilla way not the
+ * require.js way.
+ */
+exports.registerStringsSource = function(modulePath) {
+  throw new Error('registerStringsSource is not available in mozilla');
+};
+
+exports.unregisterStringsSource = function(modulePath) {
+  throw new Error('unregisterStringsSource is not available in mozilla');
+};
+
+exports.lookupSwap = function(key, swaps) {
+  throw new Error('lookupSwap is not available in mozilla');
+};
+
+exports.lookupPlural = function(key, ord, swaps) {
+  throw new Error('lookupPlural is not available in mozilla');
+};
+
+exports.getPreferredLocales = function() {
+  return [ 'root' ];
+};
+
+/** @see lookup() in lib/util/l10n.js */
+exports.lookup = function(key) {
+  try {
+    // Our memory leak hunter walks reachable objects trying to work out what
+    // type of thing they are using object.constructor.name. If that causes
+    // problems then we can avoid the unknown-key-exception with the following:
+    /*
+    if (key === 'constructor') {
+      return { name: 'l10n-mem-leak-defeat' };
+    }
+    */
+
+    return imports.stringBundle.GetStringFromName(key);
+  }
+  catch (ex) {
+    console.error('Failed to lookup ', key, ex);
+    return key;
+  }
+};
+
+/** @see propertyLookup in lib/util/l10n.js */
+exports.propertyLookup = Proxy.create({
+  get: function(rcvr, name) {
+    return exports.lookup(name);
+  }
+});
+
+/** @see lookupFormat in lib/util/l10n.js */
+exports.lookupFormat = function(key, swaps) {
+  try {
+    return imports.stringBundle.formatStringFromName(key, swaps, swaps.length);
+  }
+  catch (ex) {
+    console.error('Failed to format ', key, ex);
+    return key;
+  }
+};
+
+
+});
+/*
+ * 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/promise'], function(require, exports, module) {
+
+'use strict';
+
+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) {
+    return conversionContext.document.createTextNode(data || '');
+  }
+};
+
+/**
+ * Best guess at creating a string from random data
+ */
+var fallbackStringConverter = {
+  from: '*',
+  to: 'string',
+  exec: function(data, conversionContext) {
+    return data == null ? '' : data.toString();
+  }
+};
+
+/**
+ * Convert a view object to a DOM element
+ */
+var viewDomConverter = {
+  item: 'converter',
+  from: 'view',
+  to: 'dom',
+  exec: function(view, conversionContext) {
+    return view.toDom(conversionContext.document);
+  }
+};
+
+/**
+ * Convert a view object to a string
+ */
+var viewStringConverter = {
+  item: 'converter',
+  from: 'view',
+  to: 'string',
+  exec: function(view, conversionContext) {
+    return view.toDom(conversionContext.document).textContent;
+  }
+};
+
+/**
+ * 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, conversionContext) {
+      var intermediate = first.exec(data, conversionContext);
+      return second.exec(intermediate, conversionContext);
+    }
+  };
+}
+
+/**
+ * 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));
+};
+
+/**
+ * Items for export
+ */
+exports.items = [ viewDomConverter, viewStringConverter ];
+
+
+});
+/*
+ * 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/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;
+
+/**
+ * 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
+ * - named: Is this parameter named? That is to say, are positional
+ *         arguments disallowed, if true, then we need to provide updates to
+ *         the command line that explicitly name the parameter in use
+ *         (e.g. --verbose, or --name Fred rather than just true or Fred)
+ * - name: If this parameter is named, what name should we use
+ * - requisition: The requisition that we're attached to
+ * - required: Boolean to indicate if this is a mandatory field
+ */
+function Field(type, options) {
+  this.type = type;
+  this.document = options.document;
+  this.requisition = options.requisition;
+}
+
+/**
+ * Enable registration of fields using addItems
+ */
+Field.prototype.item = 'field';
+
+/**
+ * Subclasses should assign their element with the DOM node that gets added
+ * to the 'form'. It doesn't have to be an input node, just something that
+ * contains it.
+ */
+Field.prototype.element = undefined;
+
+/**
+ * Indicates that this field should drop any resources that it has created
+ */
+Field.prototype.destroy = function() {
+  delete this.messageElement;
+};
+
+// Note: We could/should probably change Fields from working with Conversions
+// to working with Arguments (Tokens), which makes for less calls to parse()
+
+/**
+ * Update this field display with the value from this conversion.
+ * Subclasses should provide an implementation of this function.
+ */
+Field.prototype.setConversion = function(conversion) {
+  throw new Error('Field should not be used directly');
+};
+
+/**
+ * Extract a conversion from the values in this field.
+ * Subclasses should provide an implementation of this function.
+ */
+Field.prototype.getConversion = function() {
+  throw new Error('Field should not be used directly');
+};
+
+/**
+ * Set the element where messages and validation errors will be displayed
+ * @see setMessage()
+ */
+Field.prototype.setMessageElement = function(element) {
+  this.messageElement = element;
+};
+
+/**
+ * Display a validation message in the UI
+ */
+Field.prototype.setMessage = function(message) {
+  if (this.messageElement) {
+    util.setTextContent(this.messageElement, message || '');
+  }
+};
+
+/**
+ * Method to be called by subclasses when their input changes, which allows us
+ * to properly pass on the onFieldChange event.
+ */
+Field.prototype.onInputChange = function(ev) {
+  promise.resolve(this.getConversion()).then(function(conversion) {
+    this.onFieldChange({ conversion: conversion });
+    this.setMessage(conversion.message);
+
+    if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
+      this.requisition.exec();
+    }
+  }.bind(this), util.errorHandler);
+};
+
+/**
+ * Some fields contain information that is more important to the user, for
+ * example error messages and completion menus.
+ */
+Field.prototype.isImportant = false;
+
+/**
+ * 'static/abstract' method to allow implementations of Field to lay a claim
+ * to a type. This allows claims of various strength to be weighted up.
+ * See the Field.*MATCH values.
+ */
+Field.claim = function(type, context) {
+  throw new Error('Field should not be used directly');
+};
+
+/**
+ * About minimalism - If we're producing a dialog, we want a field for every
+ * parameter. If we're providing a quick tooltip, we only want a field when
+ * it's really going to help.
+ * The getField() function takes an option of 'tooltip: true'. Fields are
+ * expected to reply with a TOOLTIP_* constant if they should be shown in the
+ * tooltip case.
+ */
+Field.TOOLTIP_MATCH = 5;   // A best match, that works for a tooltip
+Field.TOOLTIP_DEFAULT = 4; // A default match that should show in a tooltip
+Field.MATCH = 3;           // Match, but ignorable if we're being minimalist
+Field.DEFAULT = 2;         // This is a default (non-minimalist) match
+Field.BASIC = 1;           // OK in an emergency. i.e. assume Strings
+Field.NO_MATCH = 0;        // This field can't help with the given type
+
+exports.Field = Field;
+
+
+/**
+ * Internal array of known fields
+ */
+var fieldCtors = [];
+
+/**
+ * Add a field definition by field constructor
+ * @param fieldCtor Constructor function of new Field
+ */
+exports.addField = function(fieldCtor) {
+  if (typeof fieldCtor !== 'function') {
+    console.error('addField erroring on ', fieldCtor);
+    throw new Error('addField requires a Field constructor');
+  }
+  fieldCtors.push(fieldCtor);
+};
+
+/**
+ * Remove a Field definition
+ * @param field A previously registered field, specified either with a field
+ * name or from the field name
+ */
+exports.removeField = function(field) {
+  if (typeof field !== 'string') {
+    fields = fields.filter(function(test) {
+      return test !== field;
+    });
+    delete fields[field];
+  }
+  else if (field instanceof Field) {
+    removeField(field.name);
+  }
+  else {
+    console.error('removeField erroring on ', field);
+    throw new Error('removeField requires an instance of Field');
+  }
+};
+
+/**
+ * Find the best possible matching field from the specification of the type
+ * of field required.
+ * @param type An instance of Type that we will represent
+ * @param options A set of properties that we should attempt to match, and use
+ * in the construction of the new field object:
+ * - document: The document to use in creating new elements
+ * - name: The parameter name, (i.e. assignment.param.name)
+ * - requisition: The requisition we're monitoring,
+ * - required: Is this a required parameter (i.e. param.isDataRequired)
+ * - named: Is this a named parameters (i.e. !param.isPositionalAllowed)
+ * @return A newly constructed field that best matches the input options
+ */
+exports.getField = function(type, options) {
+  var ctor;
+  var highestClaim = -1;
+  fieldCtors.forEach(function(fieldCtor) {
+    var claim = fieldCtor.claim(type, options.requisition.executionContext);
+    if (claim > highestClaim) {
+      highestClaim = claim;
+      ctor = fieldCtor;
+    }
+  });
+
+  if (!ctor) {
+    console.error('Unknown field type ', type, ' in ', fieldCtors);
+    throw new Error('Can\'t find field for ' + type);
+  }
+
+  if (options.tooltip && highestClaim < Field.TOOLTIP_DEFAULT) {
+    return new BlankField(type, options);
+  }
+
+  return new ctor(type, options);
+};
+
+
+/**
+ * For use with delegate types that do not yet have anything to resolve to.
+ * BlankFields are not for general use.
+ */
+function BlankField(type, options) {
+  Field.call(this, 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.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.executionContext);
+};
+
+exports.addField(BlankField);
+
+
+});
+/*
+ * 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/types/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'util/spell', '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 types = require('gcli/types');
+var spell = require('util/spell');
 var Type = require('gcli/types').Type;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
-var spell = require('gcli/types/spell');
 var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(SelectionType);
-};
-
-exports.shutdown = function() {
-  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
  * selection functions. Properties include:
  * - lookup: An array of objects, one for each option, which contain name and
  *   value properties. lookup can be a function which returns this array
  * - data: An array of strings - alternative to 'lookup' where the valid values
@@ -3070,16 +4083,17 @@ SelectionType.prototype._findValue = fun
  * 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;
+exports.items = [ SelectionType ];
 
 
 });
 /*
  * 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.
@@ -3089,35 +4103,37 @@ exports.SelectionType = SelectionType;
  *
  * 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/spell', ['require', 'exports', 'module' ], function(require, exports, module) {
+define('util/spell', ['require', 'exports', 'module' ], function(require, exports, module) {
 
 'use strict';
 
 /*
  * A spell-checker based on Damerau-Levenshtein distance.
  */
 
-var INSERTION_COST = 1;
-var DELETION_COST = 1;
-var SWAP_COST = 1;
-var SUBSTITUTION_COST = 2;
-var MAX_EDIT_DISTANCE = 4;
-
-/**
- * Compute Damerau-Levenshtein Distance
+var CASE_CHANGE_COST = 1;
+var INSERTION_COST = 10;
+var DELETION_COST = 10;
+var SWAP_COST = 10;
+var SUBSTITUTION_COST = 20;
+var MAX_EDIT_DISTANCE = 40;
+
+/**
+ * Compute Damerau-Levenshtein Distance, with a modification to allow a low
+ * case-change cost (1/10th of a swap-cost)
  * @see http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance
  */
-function damerauLevenshteinDistance(wordi, wordj) {
+var distance = exports.distance = function(wordi, wordj) {
   var wordiLen = wordi.length;
   var wordjLen = wordj.length;
 
   // We only need to store three rows of our dynamic programming matrix.
   // (Without swap, it would have been two.)
   var row0 = new Array(wordiLen+1);
   var row1 = new Array(wordiLen+1);
   var row2 = new Array(wordiLen+1);
@@ -3128,239 +4144,157 @@ function damerauLevenshteinDistance(word
   // The distance between the empty string and a string of size i is the cost
   // of i insertions.
   for (i = 0; i <= wordiLen; i++) {
     row1[i] = i * INSERTION_COST;
   }
 
   // Row-by-row, we're computing the edit distance between substrings wordi[0..i]
   // and wordj[0..j].
-  for (j = 1; j <= wordjLen; j++)
-  {
+  for (j = 1; j <= wordjLen; j++) {
     // Edit distance between wordi[0..0] and wordj[0..j] is the cost of j
     // insertions.
     row0[0] = j * INSERTION_COST;
 
     for (i = 1; i <= wordiLen; i++) {
       // Handle deletion, insertion and substitution: we can reach each cell
       // from three other cells corresponding to those three operations. We
       // want the minimum cost.
-      row0[i] = Math.min(
-          row0[i-1] + DELETION_COST,
-          row1[i] + INSERTION_COST,
-          row1[i-1] + (wordi[i-1] === wordj[j-1] ? 0 : SUBSTITUTION_COST));
+      var dc = row0[i - 1] + DELETION_COST;
+      var ic = row1[i] + INSERTION_COST;
+      var sc0;
+      if (wordi[i-1] === wordj[j-1]) {
+        sc0 = 0;
+      }
+      else {
+        if (wordi[i-1].toLowerCase() === wordj[j-1].toLowerCase()) {
+          sc0 = CASE_CHANGE_COST;
+        }
+        else {
+          sc0 = SUBSTITUTION_COST;
+        }
+      }
+      var sc = row1[i-1] + sc0;
+
+      row0[i] = Math.min(dc, ic, sc);
+
       // We handle swap too, eg. distance between help and hlep should be 1. If
       // we find such a swap, there's a chance to update row0[1] to be lower.
       if (i > 1 && j > 1 && wordi[i-1] === wordj[j-2] && wordj[j-1] === wordi[i-2]) {
         row0[i] = Math.min(row0[i], row2[i-2] + SWAP_COST);
       }
     }
 
     tmp = row2;
     row2 = row1;
     row1 = row0;
     row0 = tmp;
   }
 
   return row1[wordiLen];
-}
+};
+
+/**
+ * As distance() except that we say that if word is a prefix of name then we
+ * only count the case changes. This allows us to use words that can be
+ * completed by typing as more likely than short words
+ */
+var distancePrefix = exports.distancePrefix = function(word, name) {
+  var dist = 0;
+
+  for (var i = 0; i < word.length; i++) {
+    if (name[i] !== word[i]) {
+      if (name[i].toLowerCase() === word[i].toLowerCase()) {
+        dist++;
+      }
+      else {
+        // name does not start with word, even ignoring case, use
+        // Damerau-Levenshtein
+        return exports.distance(word, name);
+      }
+    }
+  }
+
+  return dist;
+};
 
 /**
  * A function that returns the correction for the specified word.
  */
 exports.correct = function(word, names) {
   if (names.length === 0) {
     return undefined;
   }
 
-  var distance = {};
+  var distances = {};
   var sortedCandidates;
 
   names.forEach(function(candidate) {
-    distance[candidate] = damerauLevenshteinDistance(word, candidate);
+    distances[candidate] = exports.distance(word, candidate);
   });
 
   sortedCandidates = names.sort(function(worda, wordb) {
-    if (distance[worda] !== distance[wordb]) {
-      return distance[worda] - distance[wordb];
+    if (distances[worda] !== distances[wordb]) {
+      return distances[worda] - distances[wordb];
     }
     else {
       // if the score is the same, always return the first string
       // in the lexicographical order
       return worda < wordb;
     }
   });
 
-  if (distance[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) {
+  if (distances[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) {
     return sortedCandidates[0];
   }
   else {
     return 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/types/command', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/canon', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) {
-
-'use strict';
-
-var promise = require('util/promise');
-var l10n = require('util/l10n');
-var canon = require('gcli/canon');
-var types = require('gcli/types');
-var SelectionType = require('gcli/types/selection').SelectionType;
-var Status = require('gcli/types').Status;
-var Conversion = require('gcli/types').Conversion;
-
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(CommandType);
-  types.addType(ParamType);
-};
-
-exports.shutdown = function() {
-  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.
- * If you are making changes to this code, you should check there too.
- */
-function ParamType(typeSpec) {
-  this.requisition = typeSpec.requisition;
-  this.isIncompleteName = typeSpec.isIncompleteName;
-  this.stringifyProperty = 'name';
-  this.neverForceAsync = true;
-}
-
-ParamType.prototype = Object.create(SelectionType.prototype);
-
-ParamType.prototype.name = 'param';
-
-ParamType.prototype.lookup = function() {
-  var displayedParams = [];
-  var command = this.requisition.commandAssignment.value;
-  if (command != null) {
-    command.params.forEach(function(param) {
-      var arg = this.requisition.getAssignment(param.name).arg;
-      if (!param.isPositionalAllowed && arg.type === "BlankArgument") {
-        displayedParams.push({ name: '--' + param.name, value: param });
-      }
-    }, this);
-  }
-  return displayedParams;
-};
-
-ParamType.prototype.parse = function(arg, context) {
-  if (this.isIncompleteName) {
-    return SelectionType.prototype.parse.call(this, arg, context);
-  }
-  else {
-    var message = l10n.lookup('cliUnusedArg');
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-  }
-};
-
-
-/**
- * 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.
- * If you are making changes to this code, you should check there too.
- */
-function CommandType() {
-  this.stringifyProperty = 'name';
-  this.neverForceAsync = true;
-}
-
-CommandType.prototype = Object.create(SelectionType.prototype);
-
-CommandType.prototype.name = 'command';
-
-CommandType.prototype.lookup = function() {
-  var commands = canon.getCommands();
-  commands.sort(function(c1, c2) {
-    return c1.name.localeCompare(c2.name);
+/**
+ * Return a ranked list of matches:
+ *
+ *   spell.rank('fred', [ 'banana', 'fred', 'ed', 'red' ]);
+ *     ↓
+ *   [
+ *      { name: 'fred', dist: 0 },
+ *      { name: 'red', dist: 1 },
+ *      { name: 'ed', dist: 2 },
+ *      { name: 'banana', dist: 10 },
+ *   ]
+ *
+ * @param word The string that we're comparing names against
+ * @param names An array of strings to compare word against
+ * @param options Comparison options:
+ * - noSort: Do not sort the output by distance
+ * - prefixZero: Count prefix matches as edit distance 0 (i.e. word='bana' and
+ *   names=['banana'], would return { name:'banana': dist: 0 }) This is useful
+ *   if someone is typing the matches and may not have finished yet
+ */
+exports.rank = function(word, names, options) {
+  options = options || {};
+
+  var reply = names.map(function(name) {
+    // If any name starts with the word then the distance is based on the
+    // number of case changes rather than Damerau-Levenshtein
+    var algo = options.prefixZero ? distancePrefix : distance;
+    return {
+      name: name,
+      dist: algo(word, name)
+    };
   });
-  return commands.map(function(command) {
-    return { name: command.name, value: command };
-  }, this);
-};
-
-/**
- * Add an option to our list of predicted options
- */
-CommandType.prototype._addToPredictions = function(predictions, option, arg) {
-  // The command type needs to exclude sub-commands when the CLI
-  // is blank, but include them when we're filtering. This hack
-  // excludes matches when the filter text is '' and when the
-  // name includes a space.
-  if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) {
-    predictions.push(option);
-  }
-};
-
-CommandType.prototype.parse = function(arg, context) {
-  // Especially at startup, predictions live over the time that things change
-  // so we provide a completion function rather than completion values
-  var predictFunc = function() {
-    return this._findPredictions(arg);
-  }.bind(this);
-
-  return this._findPredictions(arg).then(function(predictions) {
-    if (predictions.length === 0) {
-      var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]);
-      return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc);
-    }
-
-    var command = predictions[0].value;
-
-    if (predictions.length === 1) {
-      // Is it an exact match of an executable command,
-      // or just the only possibility?
-      if (command.name === arg.text && typeof command.exec === 'function') {
-        return new Conversion(command, arg, Status.VALID, '');
-      }
-
-      return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc);
-    }
-
-    // It's valid if the text matches, even if there are several options
-    if (predictions[0].name === arg.text) {
-      return new Conversion(command, arg, Status.VALID, '', predictFunc);
-    }
-
-    return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc);
-  }.bind(this));
+
+  if (!options.noSort) {
+    reply = reply.sort(function(d1, d2) {
+      return d1.dist - d2.dist;
+    });
+  }
+
+  return reply;
 };
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -3371,551 +4305,401 @@ 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'], function(require, exports, module) {
+define('gcli/types/delegate', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+var promise = require('util/promise');
+var Conversion = require('gcli/types').Conversion;
+
+/**
+ * A type for "we don't know right now, but hope to soon"
+ */
+var delegate = {
+  item: 'type',
+  name: 'delegate',
+
+  constructor: function() {
+    if (typeof this.delegateType !== 'function') {
+      throw new Error('Instances of DelegateType need typeSpec.delegateType' +
+                      ' to be a function that returns a type');
+    }
+  },
+
+  // Child types should implement this method to return an instance of the type
+  // that should be used. If no type is available, or some sort of temporary
+  // placeholder is required, BlankType can be used.
+  delegateType: function(context) {
+    throw new Error('Not implemented');
+  },
+
+  stringify: function(value, context) {
+    return this.delegateType(context).stringify(value, context);
+  },
+
+  parse: function(arg, context) {
+    return this.delegateType(context).parse(arg, context);
+  },
+
+  decrement: function(value, context) {
+    var delegated = this.delegateType(context);
+    return (delegated.decrement ? delegated.decrement(value, context) : undefined);
+  },
+
+  increment: function(value, context) {
+    var delegated = this.delegateType(context);
+    return (delegated.increment ? delegated.increment(value, context) : undefined);
+  },
+
+  getType: function(context) {
+    return this.delegateType(context);
+  },
+
+  // DelegateType is designed to be inherited from, so DelegateField needs a way
+  // to check if something works like a delegate without using 'name'
+  isDelegate: true,
+};
+
+Object.defineProperty(delegate, 'isImportant', {
+  get: function() {
+    return this.delegateType().isImportant;
+  },
+  enumerable: true
+});
+
+/**
+ * 'blank' is a type for use with DelegateType when we don't know yet.
+ * It should not be used anywhere else.
+ */
+var blank = {
+  item: 'type',
+  name: 'blank',
+
+  stringify: function(value, context) {
+    return '';
+  },
+
+  parse: function(arg, context) {
+    return promise.resolve(new Conversion(undefined, arg));
+  }
+};
+
+/**
+ * The types we expose for registration
+ */
+exports.items = [ delegate, blank ];
+
+
+});
+/*
+ * 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/types/array', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/argument'], function(require, exports, module) {
+
+'use strict';
+
+var promise = require('util/promise');
+var types = require('gcli/types');
+var ArrayConversion = require('gcli/types').ArrayConversion;
+var ArrayArgument = require('gcli/argument').ArrayArgument;
+
+exports.items = [
+  {
+    // A set of objects of the same type
+    item: 'type',
+    name: 'array',
+    subtype: undefined,
+
+    constructor: function() {
+      if (!this.subtype) {
+        console.error('Array.typeSpec is missing subtype. Assuming string.' +
+            this.name);
+        this.subtype = 'string';
+      }
+      this.subtype = types.createType(this.subtype);
+    },
+
+    stringify: function(values, context) {
+      if (values == null) {
+        return '';
+      }
+      // BUG 664204: Check for strings with spaces and add quotes
+      return values.join(' ');
+    },
+
+    parse: function(arg, context) {
+      if (arg.type !== 'ArrayArgument') {
+        console.error('non ArrayArgument to ArrayType.parse', arg);
+        throw new Error('non ArrayArgument to ArrayType.parse');
+      }
+
+      // Parse an argument to a conversion
+      // Hack alert. ArrayConversion needs to be able to answer questions about
+      // the status of individual conversions in addition to the overall state.
+      // |subArg.conversion| allows us to do that easily.
+      var subArgParse = function(subArg) {
+        return this.subtype.parse(subArg, context).then(function(conversion) {
+          subArg.conversion = conversion;
+          return conversion;
+        }.bind(this));
+      }.bind(this);
+
+      var conversionPromises = arg.getArguments().map(subArgParse);
+      return promise.all(conversionPromises).then(function(conversions) {
+        return new ArrayConversion(conversions, arg);
+      });
+    },
+
+    getBlank: function() {
+      return new ArrayConversion([], new ArrayArgument());
+    }
+  },
+];
+
+
+});
+/*
+ * 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/types/boolean', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) {
 
 'use strict';
 
 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;
-
-/**
- * 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.
- */
-function lookup(data, onUndefined) {
-  if (data == null) {
-    if (onUndefined) {
-      return l10n.lookup(onUndefined);
-    }
-
-    return data;
-  }
-
-  if (typeof data === 'string') {
-    return data;
-  }
-
-  if (typeof data === 'object') {
-    if (data.key) {
-      return l10n.lookup(data.key);
-    }
-
-    var locales = l10n.getPreferredLocales();
-    var translated;
-    locales.some(function(locale) {
-      translated = data[locale];
-      return translated != null;
-    });
-    if (translated != null) {
-      return translated;
-    }
-
-    console.error('Can\'t find locale in descriptions: ' +
-            'locales=' + JSON.stringify(locales) + ', ' +
-            'description=' + JSON.stringify(data));
-    return '(No description)';
-  }
-
-  return l10n.lookup(onUndefined);
-}
-
-
-/**
- * The command object is mostly just setup around a commandSpec (as passed to
- * #addCommand()).
- */
-function Command(commandSpec) {
-  Object.keys(commandSpec).forEach(function(key) {
-    this[key] = commandSpec[key];
-  }, this);
-
-  if (!this.name) {
-    throw new Error('All registered commands must have a name');
-  }
-
-  if (this.params == null) {
-    this.params = [];
-  }
-  if (!Array.isArray(this.params)) {
-    throw new Error('command.params must be an array in ' + this.name);
-  }
-
-  this.hasNamedParameters = false;
-  this.description = 'description' in this ? this.description : undefined;
-  this.description = lookup(this.description, 'canonDescNone');
-  this.manual = 'manual' in this ? this.manual : undefined;
-  this.manual = lookup(this.manual);
-
-  // At this point this.params has nested param groups. We want to flatten it
-  // out and replace the param object literals with Parameter objects
-  var paramSpecs = this.params;
-  this.params = [];
-
-  // Track if the user is trying to mix default params and param groups.
-  // All the non-grouped parameters must come before all the param groups
-  // because non-grouped parameters can be assigned positionally, so their
-  // index is important. We don't want 'holes' in the order caused by
-  // parameter groups.
-  var usingGroups = false;
-
-  // In theory this could easily be made recursive, so param groups could
-  // contain nested param groups. Current thinking is that the added
-  // complexity for the UI probably isn't worth it, so this implementation
-  // prevents nesting.
-  paramSpecs.forEach(function(spec) {
-    if (!spec.group) {
-      var param = new Parameter(spec, this, null);
-      this.params.push(param);
-
-      if (!param.isPositionalAllowed) {
-        this.hasNamedParameters = true;
-      }
-
-      if (usingGroups && param.groupName == null) {
-        throw new Error('Parameters can\'t come after param groups.' +
-                        ' Ignoring ' + this.name + '/' + spec.name);
-      }
-
-      if (param.groupName != null) {
-        usingGroups = true;
-      }
-    }
-    else {
-      spec.params.forEach(function(ispec) {
-        var param = new Parameter(ispec, this, spec.group);
-        this.params.push(param);
-
-        if (!param.isPositionalAllowed) {
-          this.hasNamedParameters = true;
-        }
-      }, this);
-
-      usingGroups = true;
-    }
-  }, this);
-}
-
-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' };
-  this.paramSpec = paramSpec;
-  this.name = this.paramSpec.name;
-  this.type = this.paramSpec.type;
-
-  this.groupName = groupName;
-  if (this.groupName != null) {
-    if (this.paramSpec.option != null) {
-      throw new Error('Can\'t have a "option" property in a nested parameter');
-    }
-  }
-  else {
-    if (this.paramSpec.option != null) {
-      this.groupName = this.paramSpec.option === true ?
-              l10n.lookup('canonDefaultGroupName') :
-              '' + this.paramSpec.option;
-    }
-  }
-
-  if (!this.name) {
-    throw new Error('In ' + this.command.name +
-                    ': all params must have a name');
-  }
-
-  var typeSpec = this.type;
-  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.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
-  // there is no defaultValue, and null is used when the parameter is
-  // optional, neither are required to parse and stringify.
-  if (this._defaultValue != null) {
-    try {
-      // Passing null in for a context is bound to get us into trouble some day
-      // in which case we'll need to mock one up in some way
-      var context = null;
-      var defaultText = this.type.stringify(this.paramSpec.defaultValue, context);
-      var parsed = this.type.parseString(defaultText, context);
-      parsed.then(function(defaultConversion) {
-        if (defaultConversion.getStatus() !== Status.VALID) {
-          console.error('In ' + this.command.name + '/' + this.name +
-                        ': Error round tripping defaultValue. status = ' +
-                        defaultConversion.getStatus());
-        }
-      }.bind(this), util.errorHandler);
-    }
-    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.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
- */
-Object.defineProperty(Parameter.prototype, 'defaultValue', {
-  get: function() {
-    if (!('_defaultValue' in this)) {
-      this._defaultValue = (this.paramSpec.defaultValue !== undefined) ?
-          this.paramSpec.defaultValue :
-          this.type.getBlank().value;
-    }
-
-    return this._defaultValue;
-  },
-  enumerable : true
-});
-
-/**
- * Does the given name uniquely identify this param (among the other params
- * in this command)
- * @param name The name to check
- */
-Parameter.prototype.isKnownAs = function(name) {
-  if (name === '--' + this.name) {
-    return true;
-  }
-  return false;
-};
-
-/**
- * Resolve the manual for this parameter, by looking in the paramSpec
- * and doing a l10n lookup
- */
-Object.defineProperty(Parameter.prototype, 'manual', {
-  get: function() {
-    return lookup(this.paramSpec.manual || undefined);
-  },
-  enumerable: true
-});
-
-/**
- * Resolve the description for this parameter, by looking in the paramSpec
- * and doing a l10n lookup
- */
-Object.defineProperty(Parameter.prototype, 'description', {
-  get: function() {
-    return lookup(this.paramSpec.description || undefined, 'canonDescNone');
-  },
-  enumerable: true
-});
-
-/**
- * Is the user required to enter data for this parameter? (i.e. has
- * defaultValue been set to something other than undefined)
- */
-Object.defineProperty(Parameter.prototype, 'isDataRequired', {
-  get: function() {
-    return this.defaultValue === undefined;
-  },
-  enumerable: true
-});
-
-/**
- * Reflect the paramSpec 'hidden' property (dynamically so it can change)
- */
-Object.defineProperty(Parameter.prototype, 'hidden', {
-  get: function() {
-    return this.paramSpec.hidden;
-  },
-  enumerable: true
-});
-
-/**
- * Are we allowed to assign data to this parameter using positional
- * parameters?
- */
-Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', {
-  get: function() {
-    return this.groupName == null;
-  },
-  enumerable: true
-});
-
-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.prototype.addCommand = function(commandSpec) {
-  if (this._commands[commandSpec.name] != null) {
-    // Roughly canon.removeCommand() without the event call, which we do later
-    delete this._commands[commandSpec.name];
-    this._commandNames = this._commandNames.filter(function(test) {
-      return test !== commandSpec.name;
-    });
-  }
-
-  var command = new Command(commandSpec);
-  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.prototype.removeCommand = function(commandOrName) {
-  var name = typeof commandOrName === 'string' ?
-          commandOrName :
-          commandOrName.name;
-
-  if (!this._commands[name]) {
-    return false;
-  }
-
-  // See start of canon.addCommand if changing this code
-  delete this._commands[name];
-  delete this._commandSpecs[name];
-  this._commandNames = this._commandNames.filter(function(test) {
-    return test !== name;
-  });
-
-  this.onCanonChange();
-  return true;
-};
-
-/**
- * Retrieve a command by name
- * @param name The name of the command to retrieve
- */
-Canon.prototype.getCommand = function(name) {
-  // '|| undefined' is to silence 'reference to undefined property' warnings
-  return this._commands[name] || undefined;
-};
-
-/**
- * Get an array of all the registered commands.
- */
-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.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.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.noRemote) {
-      return;
-    }
-
-    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');
-}
-
-exports.CommandOutputManager = CommandOutputManager;
+var Conversion = require('gcli/types').Conversion;
+var SelectionType = require('gcli/types/selection').SelectionType;
+
+var BlankArgument = require('gcli/argument').BlankArgument;
+
+exports.items = [
+  {
+    // 'boolean' type
+    item: 'type',
+    name: 'boolean',
+    parent: 'selection',
+
+    lookup: [
+      { name: 'false', value: false },
+      { name: 'true', value: true }
+    ],
+
+    parse: function(arg, context) {
+      if (arg.type === 'TrueNamedArgument') {
+        return promise.resolve(new Conversion(true, arg));
+      }
+      if (arg.type === 'FalseNamedArgument') {
+        return promise.resolve(new Conversion(false, arg));
+      }
+      return SelectionType.prototype.parse.call(this, arg, context);
+    },
+
+    stringify: function(value, context) {
+      if (value == null) {
+        return '';
+      }
+      return '' + value;
+    },
+
+    getBlank: function(context) {
+      return new Conversion(false, new BlankArgument(), Status.VALID, '',
+                            promise.resolve(this.lookup));
+    }
+  }
+];
 
 
 });
 /*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
+ * 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/types/command', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/canon', 'gcli/types/selection', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+var promise = require('util/promise');
+var l10n = require('util/l10n');
+var canon = require('gcli/canon');
+var SelectionType = require('gcli/types/selection').SelectionType;
+var Status = require('gcli/types').Status;
+var Conversion = require('gcli/types').Conversion;
+
+exports.items = [
+  {
+    // Select from the available parameters to a command
+    item: 'type',
+    name: 'param',
+    parent: 'selection',
+    stringifyProperty: 'name',
+    neverForceAsync: true,
+    requisition: undefined,
+    isIncompleteName: undefined,
+
+    lookup: function() {
+      var displayedParams = [];
+      var command = this.requisition.commandAssignment.value;
+      if (command != null) {
+        command.params.forEach(function(param) {
+          var arg = this.requisition.getAssignment(param.name).arg;
+          if (!param.isPositionalAllowed && arg.type === "BlankArgument") {
+            displayedParams.push({ name: '--' + param.name, value: param });
+          }
+        }, this);
+      }
+      return displayedParams;
+    },
+
+    parse: function(arg, context) {
+      if (this.isIncompleteName) {
+        return SelectionType.prototype.parse.call(this, arg, context);
+      }
+      else {
+        var message = l10n.lookup('cliUnusedArg');
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+      }
+    }
+  },
+  {
+    // 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.
+    // If you are making changes to this code, you should check there too.
+    item: 'type',
+    name: 'command',
+    parent: 'selection',
+    stringifyProperty: 'name',
+    neverForceAsync: true,
+
+    lookup: function() {
+      var commands = canon.getCommands();
+      commands.sort(function(c1, c2) {
+        return c1.name.localeCompare(c2.name);
+      });
+      return commands.map(function(command) {
+        return { name: command.name, value: command };
+      }, this);
+    },
+
+    // Add an option to our list of predicted options
+    _addToPredictions: function(predictions, option, arg) {
+      // The command type needs to exclude sub-commands when the CLI
+      // is blank, but include them when we're filtering. This hack
+      // excludes matches when the filter text is '' and when the
+      // name includes a space.
+      if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) {
+        predictions.push(option);
+      }
+    },
+
+    parse: function(arg, context) {
+      // Especially at startup, predictions live over the time that things change
+      // so we provide a completion function rather than completion values
+      var predictFunc = function() {
+        return this._findPredictions(arg);
+      }.bind(this);
+
+      return this._findPredictions(arg).then(function(predictions) {
+        if (predictions.length === 0) {
+          var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]);
+          return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc);
+        }
+
+        var command = predictions[0].value;
+
+        if (predictions.length === 1) {
+          // Is it an exact match of an executable command,
+          // or just the only possibility?
+          if (command.name === arg.text && typeof command.exec === 'function') {
+            return new Conversion(command, arg, Status.VALID, '');
+          }
+
+          return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc);
+        }
+
+        // It's valid if the text matches, even if there are several options
+        if (predictions[0].name === arg.text) {
+          return new Conversion(command, arg, Status.VALID, '', predictFunc);
+        }
+
+        return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc);
+      }.bind(this));
+    }
+  }
+];
+
+});
+/*
+ * 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/types/date', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) {
 
 'use strict';
 
 var promise = require('util/promise');
 var l10n = require('util/l10n');
-
-var types = require('gcli/types');
 var Type = require('gcli/types').Type;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
 
 
 function DateType(typeSpec) {
   // ECMA 5.1 §15.9.1.1
   // @see http://stackoverflow.com/questions/11526504/minimum-and-maximum-date
@@ -4086,16 +4870,18 @@ DateType.prototype.increment = function(
   }
   else {
     return this.getMax();
   }
 };
 
 DateType.prototype.name = 'date';
 
+exports.items = [ DateType ];
+
 
 /**
  * Utility to convert a string to a date, throwing if the date can't be
  * parsed rather than having an invalid date
  */
 function toDate(str) {
   var millis = Date.parse(str);
   if (isNaN(millis)) {
@@ -4109,27 +4895,551 @@ function toDate(str) {
  * @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
  */
 function isDate(thing) {
   return Object.prototype.toString.call(thing) === '[object Date]'
           && !isNaN(thing.getTime());
 };
 
 
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(DateType);
-};
-
-exports.shutdown = function() {
-  types.removeType(DateType);
-};
-
+});
+/*
+ * 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/types/file', ['require', 'exports', 'module' , 'gcli/types/fileparser', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+/*
+ * The file type is a bit of a spiders-web, but there isn't a nice solution
+ * yet. The core of the problem is that the modules used by Firefox and NodeJS
+ * intersect with the modules used by the web, but not each other. Except here.
+ * So we have to do something fancy to get the sharing but not mess up the web.
+ *
+ * This file requires 'gcli/types/fileparser', and there are 4 implementations
+ * of this:
+ * - '/lib/gcli/types/fileparser.js', the default web version that uses XHR to
+ *   talk to the node server
+ * - '/lib/server/gcli/types/fileparser.js', an NodeJS stub, and ...
+ * - '/mozilla/gcli/types/fileparser.js', the Firefox implementation both of
+ *   these are shims which import
+ * - 'util/fileparser', which does the real work, except the actual file access
+ *
+ * The file access comes from the 'util/filesystem' module, and there are 2
+ * implementations of this:
+ * - '/lib/server/util/filesystem.js', which uses NodeJS APIs
+ * - '/mozilla/util/filesystem.js', which uses OS.File APIs
+ */
+
+var fileparser = require('gcli/types/fileparser');
+var Conversion = require('gcli/types').Conversion;
+
+exports.items = [
+  {
+    item: 'type',
+    name: 'file',
+
+    filetype: 'any',    // One of 'file', 'directory', 'any'
+    existing: 'maybe',  // Should be one of 'yes', 'no', 'maybe'
+    matches: undefined, // RegExp to match the file part of the path
+
+    isSelection: true,  // It's not really a selection, but acts like one
+
+    constructor: function() {
+      if (this.filetype !== 'any' && this.filetype !== 'file' &&
+          this.filetype !== 'directory') {
+        throw new Error('filetype must be one of [any|file|directory]');
+      }
+
+      if (this.existing !== 'yes' && this.existing !== 'no' &&
+          this.existing !== 'maybe') {
+        throw new Error('existing must be one of [yes|no|maybe]');
+      }
+    },
+
+    stringify: function(file) {
+      if (file == null) {
+        return '';
+      }
+
+      return file.toString();
+    },
+
+    parse: function(arg, context) {
+      var options = {
+        filetype: this.filetype,
+        existing: this.existing,
+        matches: this.matches
+      };
+      var promise = fileparser.parse(arg.text, options);
+
+      return promise.then(function(reply) {
+        return new Conversion(reply.value, arg, reply.status,
+                              reply.message, reply.predictor);
+      });
+    }
+  }
+];
+
+});
+/*
+ * 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/types/fileparser', ['require', 'exports', 'module' , 'util/fileparser'], function(require, exports, module) {
+
+'use strict';
+
+var fileparser = require('util/fileparser');
+
+fileparser.supportsPredictions = false;
+exports.parse = fileparser.parse;
+
+});
+/*
+ * 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/fileparser', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'util/spell', 'util/filesystem', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+var util = require('util/util');
+var l10n = require('util/l10n');
+var spell = require('util/spell');
+var filesystem = require('util/filesystem');
+var Status = require('gcli/types').Status;
+
+/*
+ * An implementation of the functions that call the filesystem, designed to
+ * support the file type.
+ * See: lib/gcli/util/filesystem.js
+ */
+
+/**
+ * Helper for the parse() function from the file type.
+ * See util/filesystem.js for details
+ */
+exports.parse = function(typed, options) {
+  return filesystem.stat(typed).then(function(stats) {
+    // The 'save-as' case - the path should not exist but does
+    if (options.existing === 'no' && stats.exists) {
+      return {
+        value: undefined,
+        status: Status.INCOMPLETE,
+        message: l10n.lookupFormat('fileErrExists', [ typed ]),
+        predictor: undefined // No predictions that we can give here
+      };
+    }
+
+    if (stats.exists) {
+      // The path exists - check it's the correct file type ...
+      if (options.filetype === 'file' && !stats.isFile) {
+        return {
+          value: undefined,
+          status: Status.INCOMPLETE,
+          message: l10n.lookupFormat('fileErrIsNotFile', [ typed ]),
+          predictor: getPredictor(typed, options)
+        };
+      }
+
+      if (options.filetype === 'directory' && !stats.isDir) {
+        return {
+          value: undefined,
+          status: Status.INCOMPLETE,
+          message: l10n.lookupFormat('fileErrIsNotDirectory', [ typed ]),
+          predictor: getPredictor(typed, options)
+        };
+      }
+
+      // ... and that it matches any 'match' RegExp
+      if (options.matches != null && !options.matches.test(typed)) {
+        return {
+          value: undefined,
+          status: Status.INCOMPLETE,
+          message: l10n.lookupFormat('fileErrDoesntMatch',
+                                     [ typed, options.source ]),
+          predictor: getPredictor(typed, options)
+        };
+      }
+    }
+    else {
+      if (options.existing === 'yes') {
+        // We wanted something that exists, but it doesn't. But we don't know
+        // if the path so far is an ERROR or just INCOMPLETE
+        var parentName = filesystem.dirname(typed);
+        return filesystem.stat(parentName).then(function(stats) {
+          return {
+            value: undefined,
+            status: stats.isDir ? Status.INCOMPLETE : Status.ERROR,
+            message: l10n.lookupFormat('fileErrNotExists', [ typed ]),
+            predictor: getPredictor(typed, options)
+          };
+        });
+      }
+    }
+
+    // We found no problems
+    return {
+      value: typed,
+      status: Status.VALID,
+      message: undefined,
+      predictor: getPredictor(typed, options)
+    };
+  });
+};
+
+var RANK_OPTIONS = { noSort: true, prefixZero: true };
+
+/**
+ * We want to be able to turn predictions off in Firefox
+ */
+exports.supportsPredictions = true;
+
+/**
+ * Get a function which creates predictions of files that match the given
+ * path
+ */
+function getPredictor(typed, options) {
+  if (!exports.supportsPredictions) {
+    return undefined;
+  }
+
+  return function() {
+    var allowFile = (options.filetype !== 'directory');
+    var parts = filesystem.split(typed);
+
+    var absolute = (typed.indexOf('/') === 0);
+    var roots;
+    if (absolute) {
+      roots = [ { name: '/', dist: 0, original: '/' } ];
+    }
+    else {
+      roots = history.getCommonDirectories().map(function(root) {
+        return { name: root, dist: 0, original: root };
+      });
+    }
+
+    // Add each part of the typed pathname onto each of the roots in turn,
+    // Finding options from each of those paths, and using these options as
+    // our roots for the next part
+    var partsAdded = util.promiseEach(parts, function(part, index) {
+
+      var partsSoFar = filesystem.join.apply(filesystem, parts.slice(0, index + 1));
+
+      // We allow this file matches in this pass if we're allowed files at all
+      // (i.e this isn't 'cd') and if this is the last part of the path
+      var allowFileForPart = (allowFile && index >= parts.length - 1);
+
+      var rootsPromise = util.promiseEach(roots, function(root) {
+
+        // Extend each roots to a list of all the files in each of the roots
+        var matchFile = allowFileForPart ? options.matches : null;
+        var promise = filesystem.ls(root.name, matchFile);
+
+        var onSuccess = function(entries) {
+          // Unless this is the final part filter out the non-directories
+          if (!allowFileForPart) {
+            entries = entries.filter(function(entry) {
+              return entry.isDir;
+            });
+          }
+          var entryMap = {};
+          entries.forEach(function(entry) {
+            entryMap[entry.pathname] = entry;
+          });
+          return entryMap;
+        };
+
+        var onError = function(err) {
+          // We expect errors due to the path not being a directory, not being
+          // accessible, or removed since the call to 'readdir', but other
+          // errors should be reported
+          var noComplainCodes = [ 'ENOTDIR', 'EACCES', 'EBADF', 'ENOENT' ];
+          if (noComplainCodes.indexOf(err.code) === -1) {
+            console.error('Error looing up', root.name, err);
+          }
+          return {};
+        };
+
+        promise = promise.then(onSuccess, onError);
+
+        // We want to compare all the directory entries with the original root
+        // plus the partsSoFar
+        var compare = filesystem.join(root.original, partsSoFar);
+
+        return promise.then(function(entryMap) {
+
+          var ranks = spell.rank(compare, Object.keys(entryMap), RANK_OPTIONS);
+          // penalize each path by the distance of it's parent
+          ranks.forEach(function(rank) {
+            rank.original = root.original;
+            rank.stats = entryMap[rank.name];
+          });
+          return ranks;
+        });
+      });
+
+      return rootsPromise.then(function(data) {
+        // data is an array of arrays of ranking objects. Squash down.
+        data = data.reduce(function(prev, curr) {
+          return prev.concat(curr);
+        }, []);
+
+        data.sort(function(r1, r2) {
+          return r1.dist - r2.dist;
+        });
+
+        // Trim, but by how many?
+        // If this is the last run through, we want to present the user with
+        // a sensible set of predictions. Otherwise we want to trim the tree
+        // to a reasonable set of matches, so we're happy with 1
+        // We look through x +/- 3 roots, and find the one with the biggest
+        // distance delta, and cut below that
+        // x=5 for the last time through, and x=8 otherwise
+        var isLast = index >= parts.length - 1;
+        var start = isLast ? 1 : 5;
+        var end = isLast ? 7 : 10;
+
+        var maxDeltaAt = start;
+        var maxDelta = data[start].dist - data[start - 1].dist;
+
+        for (var i = start + 1; i < end; i++) {
+          var delta = data[i].dist - data[i - 1].dist;
+          if (delta >= maxDelta) {
+            maxDelta = delta;
+            maxDeltaAt = i;
+          }
+        }
+
+        // Update the list of roots for the next time round
+        roots = data.slice(0, maxDeltaAt);
+      });
+    });
+
+    return partsAdded.then(function() {
+      var predictions = roots.map(function(root) {
+        var isFile = root.stats && root.stats.isFile;
+        var isDir = root.stats && root.stats.isDir;
+
+        var name = root.name;
+        if (isDir && name.charAt(name.length) !== filesystem.sep) {
+          name += filesystem.sep;
+        }
+
+        return {
+          name: name,
+          incomplete: !(allowFile && isFile),
+          isFile: isFile,  // Added for describe, below
+          dist: root.dist, // TODO: Remove - added for debug in describe
+        };
+      });
+
+      return util.promiseEach(predictions, function(prediction) {
+        if (!prediction.isFile) {
+          prediction.description = '(' + prediction.dist + ')';
+          prediction.dist = undefined;
+          prediction.isFile = undefined;
+          return prediction;
+        }
+
+        return filesystem.describe(prediction.name).then(function(description) {
+          prediction.description = description;
+          prediction.dist = undefined;
+          prediction.isFile = undefined;
+          return prediction;
+        });
+      });
+    });
+  };
+}
+
+// =============================================================================
+
+/*
+ * The idea is that we maintain a list of 'directories that the user is
+ * interested in'. We store directories in a most-frequently-used cache
+ * of some description.
+ * But for now we're just using / and ~/
+ */
+var history = {
+  getCommonDirectories: function() {
+    return [
+      filesystem.sep,  // i.e. the root directory
+      filesystem.home  // i.e. the users home directory
+    ];
+  },
+  addCommonDirectory: function(ignore) {
+    // Not implemented yet
+  }
+};
+
+
+});
+/*
+ * 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/filesystem', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) {
+
+'use strict';
+
+var OS = Components.utils.import("resource://gre/modules/osfile.jsm", {}).OS;
+var promise = require('util/promise');
+
+/**
+ * A set of functions that don't really belong in 'fs' (because they're not
+ * really universal in scope) but also kind of do (because they're not specific
+ * to GCLI
+ */
+
+exports.join = OS.Path.join;
+exports.sep = OS.Path.sep;
+exports.dirname = OS.Path.dirname;
+
+var dirService = Components.classes["@mozilla.org/file/directory_service;1"]
+                           .getService(Components.interfaces.nsIProperties);
+exports.home = dirService.get("Home", Components.interfaces.nsIFile).path;
+
+if ("winGetDrive" in OS.Path) {
+  exports.sep = '\\';
+}
+else {
+  exports.sep = '/';
+}
+
+/**
+ * Split a path into its components.
+ * @param pathname (string) The part to cut up
+ * @return An array of path components
+ */
+exports.split = function(pathname) {
+  return OS.Path.split(pathname).components;
+};
+
+/**
+ * @param pathname string, path of an existing directory
+ * @param matches optional regular expression - filter output to include only
+ * the files that match the regular expression. The regexp is applied to the
+ * filename only not to the full path
+ * @return A promise of an array of stat objects for each member of the
+ * directory pointed to by ``pathname``, each containing 2 extra properties:
+ * - pathname: The full pathname of the file
+ * - filename: The final filename part of the pathname
+ */
+exports.ls = function(pathname, matches) {
+  var iterator = new OS.File.DirectoryIterator(pathname);
+  var entries = [];
+
+  var iteratePromise = iterator.forEach(function(entry) {
+    entries.push({
+      exists: true,
+      isDir: entry.isDir,
+      isFile: !entry.isFile,
+      filename: entry.name,
+      pathname: entry.path
+    });
+  });
+
+  return iteratePromise.then(function onSuccess() {
+      iterator.close();
+      return entries;
+    },
+    function onFailure(reason) {
+      iterator.close();
+      throw reason;
+    }
+  );
+};
+
+/**
+ * stat() is annoying because it considers stat('/doesnt/exist') to be an
+ * error, when the point of stat() is to *find* *out*. So this wrapper just
+ * converts 'ENOENT' i.e. doesn't exist to { exists:false } and adds
+ * exists:true to stat blocks from existing paths
+ */
+exports.stat = function(pathname) {
+  var onResolve = function(stats) {
+    return {
+      exists: true,
+      isDir: stats.isDir,
+      isFile: !stats.isFile
+    };
+  };
+
+  var onReject = function(err) {
+    if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
+      return {
+        exists: false,
+        isDir: false,
+        isFile: false
+      };
+    }
+    throw err;
+  };
+
+  return OS.File.stat(pathname).then(onResolve, onReject);
+};
+
+/**
+ * We may read the first line of a file to describe it?
+ * Right now, however, we do nothing.
+ */
+exports.describe = function(pathname) {
+  return promise.resolve('');
+};
 
 
 });
 /*
  * 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.
@@ -4151,28 +5461,16 @@ define('gcli/types/javascript', ['requir
 var promise = require('util/promise');
 var l10n = require('util/l10n');
 var types = require('gcli/types');
 
 var Conversion = types.Conversion;
 var Type = types.Type;
 var Status = types.Status;
 
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(JavascriptType);
-};
-
-exports.shutdown = function() {
-  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') {
   globalObject = window;
 }
@@ -4683,17 +5981,17 @@ JavascriptType.prototype._isSafeProperty
 
   // The property is safe if 'get' isn't a function or if the function has a
   // prototype (in which case it's native)
   return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get;
 };
 
 JavascriptType.prototype.name = 'javascript';
 
-exports.JavascriptType = JavascriptType;
+exports.items = [ JavascriptType ];
 
 
 });
 /*
  * 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.
@@ -4710,37 +6008,22 @@ exports.JavascriptType = JavascriptType;
 
 define('gcli/types/node', ['require', 'exports', 'module' , 'util/promise', 'util/host', 'util/l10n', 'gcli/types', 'gcli/argument'], function(require, exports, module) {
 
 'use strict';
 
 var promise = require('util/promise');
 var host = require('util/host');
 var l10n = require('util/l10n');
-var types = require('gcli/types');
-var Type = require('gcli/types').Type;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
 var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(NodeType);
-  types.addType(NodeListType);
-};
-
-exports.shutdown = function() {
-  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') {
   doc = document;
 }
 
@@ -4773,128 +6056,123 @@ exports.unsetDocument = function() {
 /**
  * Getter for the document that contains the nodes we're matching
  * Most for changing things back to how they were for unit testing
  */
 exports.getDocument = function() {
   return doc;
 };
 
-
-/**
- * A CSS expression that refers to a single node
- */
-function NodeType(typeSpec) {
-}
-
-NodeType.prototype = Object.create(Type.prototype);
-
-NodeType.prototype.stringify = function(value, context) {
-  if (value == null) {
-    return '';
-  }
-  return value.__gcliQuery || 'Error';
-};
-
-NodeType.prototype.parse = function(arg, context) {
-  if (arg.text === '') {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE));
-  }
-
-  var nodes;
-  try {
-    nodes = doc.querySelectorAll(arg.text);
-  }
-  catch (ex) {
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR,
-                                          l10n.lookup('nodeParseSyntax')));
-  }
-
-  if (nodes.length === 0) {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE,
-                                          l10n.lookup('nodeParseNone')));
-  }
-
-  if (nodes.length === 1) {
-    var node = nodes.item(0);
-    node.__gcliQuery = arg.text;
-
-    host.flashNodes(node, true);
-
-    return promise.resolve(new Conversion(node, arg, Status.VALID, ''));
-  }
-
-  host.flashNodes(nodes, false);
-
-  var message = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]);
-  return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
-};
-
-NodeType.prototype.name = 'node';
-
-
-
-/**
- * A CSS expression that refers to a node list.
- *
- * The 'allowEmpty' option ensures that we do not complain if the entered CSS
- * selector is valid, but does not match any nodes. There is some overlap
- * between this option and 'defaultValue'. What the user wants, in most cases,
- * would be to use 'defaultText' (i.e. what is typed rather than the value that
- * it represents). However this isn't a concept that exists yet and should
- * probably be a part of GCLI if/when it does.
- * All NodeListTypes have an automatic defaultValue of an empty NodeList so
- * they can easily be used in named parameters.
- */
-function NodeListType(typeSpec) {
-  if ('allowEmpty' in typeSpec && typeof typeSpec.allowEmpty !== 'boolean') {
-    throw new Error('Legal values for allowEmpty are [true|false]');
-  }
-
-  this.allowEmpty = typeSpec.allowEmpty;
-}
-
-NodeListType.prototype = Object.create(Type.prototype);
-
-NodeListType.prototype.getBlank = function(context) {
-  return new Conversion(exports._empty, new BlankArgument(), Status.VALID);
-};
-
-NodeListType.prototype.stringify = function(value, context) {
-  if (value == null) {
-    return '';
-  }
-  return value.__gcliQuery || 'Error';
-};
-
-NodeListType.prototype.parse = function(arg, context) {
-  if (arg.text === '') {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE));
-  }
-
-  var nodes;
-  try {
-    nodes = doc.querySelectorAll(arg.text);
-  }
-  catch (ex) {
-    return promise.resolve(new Conversion(undefined, arg, Status.ERROR,
-                                    l10n.lookup('nodeParseSyntax')));
-  }
-
-  if (nodes.length === 0 && !this.allowEmpty) {
-    return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE,
-                                    l10n.lookup('nodeParseNone')));
-  }
-
-  host.flashNodes(nodes, false);
-  return promise.resolve(new Conversion(nodes, arg, Status.VALID, ''));
-};
-
-NodeListType.prototype.name = 'nodelist';
-
+/**
+ * The exported 'node' and 'nodelist' types
+ */
+exports.items = [
+  {
+    // The 'node' type is a CSS expression that refers to a single node
+    item: 'type',
+    name: 'node',
+
+    stringify: function(value, context) {
+      if (value == null) {
+        return '';
+      }
+      return value.__gcliQuery || 'Error';
+    },
+
+    parse: function(arg, context) {
+      if (arg.text === '') {
+        return promise.resolve(new Conversion(undefined, arg,
+                                              Status.INCOMPLETE));
+      }
+
+      var nodes;
+      try {
+        nodes = doc.querySelectorAll(arg.text);
+      }
+      catch (ex) {
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR,
+                                              l10n.lookup('nodeParseSyntax')));
+      }
+
+      if (nodes.length === 0) {
+        return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE,
+                                              l10n.lookup('nodeParseNone')));
+      }
+
+      if (nodes.length === 1) {
+        var node = nodes.item(0);
+        node.__gcliQuery = arg.text;
+
+        host.flashNodes(node, true);
+
+        return promise.resolve(new Conversion(node, arg, Status.VALID, ''));
+      }
+
+      host.flashNodes(nodes, false);
+
+      var msg = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]);
+      return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+    }
+  },
+  {
+    // The 'nodelist' type is a CSS expression that refers to a node list
+    item: 'type',
+    name: 'nodelist',
+
+    // The 'allowEmpty' option ensures that we do not complain if the entered
+    // CSS selector is valid, but does not match any nodes. There is some
+    // overlap between this option and 'defaultValue'. What the user wants, in
+    // most cases, would be to use 'defaultText' (i.e. what is typed rather than
+    // the value that it represents). However this isn't a concept that exists
+    // yet and should probably be a part of GCLI if/when it does.
+    // All NodeListTypes have an automatic defaultValue of an empty NodeList so
+    // they can easily be used in named parameters.
+    allowEmpty: false,
+
+    constructor: function() {
+      if (typeof this.allowEmpty !== 'boolean') {
+        throw new Error('Legal values for allowEmpty are [true|false]');
+      }
+    },
+
+    getBlank: function(context) {
+      return new Conversion(exports._empty, new BlankArgument(), Status.VALID);
+    },
+
+    stringify: function(value, context) {
+      if (value == null) {
+        return '';
+      }
+      return value.__gcliQuery || 'Error';
+    },
+
+    parse: function(arg, context) {
+      if (arg.text === '') {
+        return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE));
+      }
+
+      var nodes;
+      try {
+        nodes = doc.querySelectorAll(arg.text);
+      }
+      catch (ex) {
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR,
+                                        l10n.lookup('nodeParseSyntax')));
+      }
+
+      if (nodes.length === 0 && !this.allowEmpty) {
+        return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE,
+                                        l10n.lookup('nodeParseNone')));
+      }
+
+      host.flashNodes(nodes, false);
+      return promise.resolve(new Conversion(nodes, arg, Status.VALID, ''));
+    }
+  }
+];
 
 });
 /*
  * 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
@@ -4956,37 +6234,188 @@ exports.exec = function(execSpec) {
  *
  * 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/resource', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) {
+define('gcli/types/number', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) {
 
 'use strict';
 
 var promise = require('util/promise');
-var types = require('gcli/types');
+var l10n = require('util/l10n');
+var Status = require('gcli/types').Status;
+var Conversion = require('gcli/types').Conversion;
+
+exports.items = [
+  {
+    // 'number' type
+    // Has custom max / min / step values to control increment and decrement
+    // and a boolean allowFloat property to clamp values to integers
+    item: 'type',
+    name: 'number',
+    allowFloat: false,
+    max: undefined,
+    min: undefined,
+    step: 1,
+
+    constructor: function() {
+      if (!this.allowFloat &&
+          (this._isFloat(this.min) ||
+           this._isFloat(this.max) ||
+           this._isFloat(this.step))) {
+        throw new Error('allowFloat is false, but non-integer values given in type spec');
+      }
+    },
+
+    stringify: function(value, context) {
+      if (value == null) {
+        return '';
+      }
+      return '' + value;
+    },
+
+    getMin: function(context) {
+      if (this.min) {
+        if (typeof this.min === 'function') {
+          return this.min(context);
+        }
+        if (typeof this.min === 'number') {
+          return this.min;
+        }
+      }
+      return undefined;
+    },
+
+    getMax: function(context) {
+      if (this.max) {
+        if (typeof this.max === 'function') {
+          return this.max(context);
+        }
+        if (typeof this.max === 'number') {
+          return this.max;
+        }
+      }
+      return undefined;
+    },
+
+    parse: function(arg, context) {
+      if (arg.text.replace(/^\s*-?/, '').length === 0) {
+        return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+      }
+
+      if (!this.allowFloat && (arg.text.indexOf('.') !== -1)) {
+        var message = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]);
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+      }
+
+      var value;
+      if (this.allowFloat) {
+        value = parseFloat(arg.text);
+      }
+      else {
+        value = parseInt(arg.text, 10);
+      }
+
+      if (isNaN(value)) {
+        var message = l10n.lookupFormat('typesNumberNan', [ arg.text ]);
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+      }
+
+      var max = this.getMax(context);
+      if (max != null && value > max) {
+        var message = l10n.lookupFormat('typesNumberMax', [ value, max ]);
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+      }
+
+      var min = this.getMin(context);
+      if (min != null && value < min) {
+        var message = l10n.lookupFormat('typesNumberMin', [ value, min ]);
+        return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+      }
+
+      return promise.resolve(new Conversion(value, arg));
+    },
+
+    decrement: function(value, context) {
+      if (typeof value !== 'number' || isNaN(value)) {
+        return this.getMax(context) || 1;
+      }
+      var newValue = value - this.step;
+      // Snap to the nearest incremental of the step
+      newValue = Math.ceil(newValue / this.step) * this.step;
+      return this._boundsCheck(newValue, context);
+    },
+
+    increment: function(value, context) {
+      if (typeof value !== 'number' || isNaN(value)) {
+        var min = this.getMin(context);
+        return min != null ? min : 0;
+      }
+      var newValue = value + this.step;
+      // Snap to the nearest incremental of the step
+      newValue = Math.floor(newValue / this.step) * this.step;
+      if (this.getMax(context) == null) {
+        return newValue;
+      }
+      return this._boundsCheck(newValue, context);
+    },
+
+    // Return the input value so long as it is within the max/min bounds.
+    // If it is lower than the minimum, return the minimum. If it is bigger
+    // than the maximum then return the maximum.
+    _boundsCheck: function(value, context) {
+      var min = this.getMin(context);
+      if (min != null && value < min) {
+        return min;
+      }
+      var max = this.getMax(context);
+      if (max != null && value > max) {
+        return max;
+      }
+      return value;
+    },
+
+    // Return true if the given value is a finite number and not an integer,
+    // else return false.
+    _isFloat: function(value) {
+      return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0));
+    }
+  }
+];
+
+
+});
+/*
+ * 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/types/resource', ['require', 'exports', 'module' , 'util/promise', 'gcli/types/selection'], function(require, exports, module) {
+
+'use strict';
+
+var promise = require('util/promise');
 var SelectionType = require('gcli/types/selection').SelectionType;
 
 
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(ResourceType);
-};
-
-exports.shutdown = function() {
-  types.removeType(ResourceType);
-  exports.clearResourceCache();
-};
-
 exports.clearResourceCache = function() {
   ResourceCache.clear();
 };
 
 /**
  * The object against which we complete, which is usually 'window' if it exists
  * but could be something else in non-web-content environments.
  */
@@ -5184,52 +6613,16 @@ function dedupe(resources, onDupe) {
     var clones = names[name];
     if (clones.length > 1) {
       onDupe(clones);
     }
   });
 }
 
 /**
- * Use the Resource implementations to create a type based on SelectionType
- */
-function ResourceType(typeSpec) {
-  this.include = typeSpec.include;
-  if (this.include !== Resource.TYPE_SCRIPT &&
-      this.include !== Resource.TYPE_CSS &&
-      this.include != null) {
-    throw new Error('invalid include property: ' + this.include);
-  }
-}
-
-ResourceType.prototype = Object.create(SelectionType.prototype);
-
-/**
- * There are several ways to get selection data. This unifies them into one
- * single function.
- * @return A map of names to values.
- */
-ResourceType.prototype.getLookup = function() {
-  var resources = [];
-  if (this.include !== Resource.TYPE_SCRIPT) {
-    Array.prototype.push.apply(resources, CssResource._getAllStyles());
-  }
-  if (this.include !== Resource.TYPE_CSS) {
-    Array.prototype.push.apply(resources, ScriptResource._getAllScripts());
-  }
-
-  return promise.resolve(resources.map(function(resource) {
-    return { name: resource.name, value: resource };
-  }));
-};
-
-ResourceType.prototype.name = 'resource';
-
-
-/**
  * A quick cache of resources against nodes
  * TODO: Potential memory leak when the target document has css or script
  * resources repeatedly added and removed. Solution might be to use a weak
  * hash map or some such.
  */
 var ResourceCache = {
   _cached: [],
 
@@ -5255,16 +6648,48 @@ var ResourceCache = {
   /**
    * Drop all cache entries. Helpful to prevent memory leaks
    */
   clear: function() {
     ResourceCache._cached = [];
   }
 };
 
+/**
+ * The resource type itself
+ */
+exports.items = [
+  {
+    item: 'type',
+    constructor: function() {
+      if (this.include !== Resource.TYPE_SCRIPT &&
+          this.include !== Resource.TYPE_CSS &&
+          this.include != null) {
+        throw new Error('invalid include property: ' + this.include);
+      }
+    },
+    name: 'resource',
+    parent: 'selection',
+    include: null,
+    cacheable: false,
+    lookup: function() {
+      var resources = [];
+      if (this.include !== Resource.TYPE_SCRIPT) {
+        Array.prototype.push.apply(resources, CssResource._getAllStyles());
+      }
+      if (this.include !== Resource.TYPE_CSS) {
+        Array.prototype.push.apply(resources, ScriptResource._getAllScripts());
+      }
+
+      return promise.resolve(resources.map(function(resource) {
+        return { name: resource.name, value: resource };
+      }));
+    }
+  }
+];
 
 });
 /*
  * 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
@@ -5280,69 +6705,146 @@ var ResourceCache = {
 
 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');
 
-/**
- * A type for selecting a known setting
- */
-var settingType = {
-  constructor: function() {
-    settings.onChange.add(function(ev) {
-      this.clearCache();
-    }, this);
+exports.items = [
+  {
+    // A type for selecting a known setting
+    item: 'type',
+    name: 'setting',
+    parent: 'selection',
+    cacheable: true,
+    constructor: function() {
+      settings.onChange.add(function(ev) {
+        this.clearCache();
+      }, this);
+    },
+    lookup: function() {
+      return settings.getAll().map(function(setting) {
+        return { name: setting.name, value: setting };
+      });
+    }
   },
-  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.addType(settingType);
-  types.addType(settingValueType);
-};
-
-exports.shutdown = function() {
-  types.removeType(settingType);
-  types.removeType(settingValueType);
-};
+  {
+    // 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
+    item: 'type',
+    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');
+    }
+  }
+];
+
+});
+/*
+ * 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/types/string', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) {
+
+'use strict';
+
+var promise = require('util/promise');
+var Status = require('gcli/types').Status;
+var Conversion = require('gcli/types').Conversion;
+
+exports.items = [
+  {
+    // 'string' the most basic string type where all we need to do is to take
+    // care of converting escaped characters like \t, \n, etc.
+    // For the full list see
+    // https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals
+    // The exception is that we ignore \b because replacing '\b' characters in
+    // stringify() with their escaped version injects '\\b' all over the place
+    // and the need to support \b seems low)
+    // Customizations:
+    // allowBlank: Allow a blank string to be counted as valid
+    item: 'type',
+    name: 'string',
+    allowBlank: false,
+
+    stringify: function(value, context) {
+      if (value == null) {
+        return '';
+      }
+
+      return value
+           .replace(/\\/g, '\\\\')
+           .replace(/\f/g, '\\f')
+           .replace(/\n/g, '\\n')
+           .replace(/\r/g, '\\r')
+           .replace(/\t/g, '\\t')
+           .replace(/\v/g, '\\v')
+           .replace(/\n/g, '\\n')
+           .replace(/\r/g, '\\r')
+           .replace(/ /g, '\\ ')
+           .replace(/'/g, '\\\'')
+           .replace(/"/g, '\\"')
+           .replace(/{/g, '\\{')
+           .replace(/}/g, '\\}');
+    },
+
+    parse:function(arg, context) {
+      if (!this.allowBlank && (arg.text == null || arg.text === '')) {
+        return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+      }
+
+      // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it
+      // is double escaped)) is first converted to a private unicode character and
+      // then at the end from \uF000 to a single '\' to avoid the string \\n being
+      // converted first to \n and then to a <LF>
+      var value = arg.text
+           .replace(/\\\\/g, '\uF000')
+           .replace(/\\f/g, '\f')
+           .replace(/\\n/g, '\n')
+           .replace(/\\r/g, '\r')
+           .replace(/\\t/g, '\t')
+           .replace(/\\v/g, '\v')
+           .replace(/\\n/g, '\n')
+           .replace(/\\r/g, '\r')
+           .replace(/\\ /g, ' ')
+           .replace(/\\'/g, '\'')
+           .replace(/\\"/g, '"')
+           .replace(/\\{/g, '{')
+           .replace(/\\}/g, '}')
+           .replace(/\uF000/g, '\\');
+
+      return promise.resolve(new Conversion(value, arg));
+    }
+  }
+];
 
 
 });
 /*
  * 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.
@@ -5352,290 +6854,130 @@ exports.shutdown = 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/settings', ['require', 'exports', 'module' , 'util/util', 'gcli/types'], function(require, exports, module) {
+define('gcli/converters/basic', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) {
 
 'use strict';
 
-var imports = {};
-
-Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports);
-
-imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() {
-  var prefService = Components.classes['@mozilla.org/preferences-service;1']
-          .getService(Components.interfaces.nsIPrefService);
-  return prefService.getBranch(null)
-          .QueryInterface(Components.interfaces.nsIPrefBranch2);
-});
-
-imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() {
-  return Components.classes["@mozilla.org/supports-string;1"]
-          .createInstance(Components.interfaces.nsISupportsString);
-});
-
-
 var util = require('util/util');
-var types = require('gcli/types');
-
-/**
- * All local settings have this prefix when used in Firefox
- */
-var DEVTOOLS_PREFIX = 'devtools.gcli.';
-
-/**
- * A class to wrap up the properties of a preference.
- * @see toolkit/components/viewconfig/content/config.js
- */
-function Setting(prefSpec) {
-  if (typeof prefSpec === 'string') {
-    // We're coming from getAll() i.e. a full listing of prefs
-    this.name = prefSpec;
-    this.description = '';
-  }
-  else {
-    // A specific addition by GCLI
-    this.name = DEVTOOLS_PREFIX + prefSpec.name;
-
-    if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) {
-      if (this.type.name !== prefSpec.type) {
-        throw new Error('Locally declared type (' + prefSpec.type + ') != ' +
-            'Mozilla declared type (' + this.type.name + ') for ' + this.name);
-      }
-    }
-
-    this.description = prefSpec.description;
-  }
-
-  this.onChange = util.createEvent('Setting.onChange');
-}
-
-/**
- * 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.createType('boolean');
-
-      case imports.prefBranch.PREF_INT:
-        return types.createType('number');
-
-      case imports.prefBranch.PREF_STRING:
-        return types.createType('string');
-
-      default:
-        throw new Error('Unknown type for ' + this.name);
-    }
+
+/**
+ * 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;
+}
+
+exports.items = [
+  {
+    item: 'converter',
+    from: 'string',
+    to: 'dom',
+    exec: nodeFromDataToString
   },
-  enumerable: true
-});
-
-/**
- * What type is this property: boolean/integer/string?
- */
-Object.defineProperty(Setting.prototype, 'value', {
-  get: function() {
-    switch (imports.prefBranch.getPrefType(this.name)) {
-      case imports.prefBranch.PREF_BOOL:
-        return imports.prefBranch.getBoolPref(this.name);
-
-      case imports.prefBranch.PREF_INT:
-        return imports.prefBranch.getIntPref(this.name);
-
-      case imports.prefBranch.PREF_STRING:
-        var value = imports.prefBranch.getComplexValue(this.name,
-                Components.interfaces.nsISupportsString).data;
-        // In case of a localized string
-        if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) {
-          value = imports.prefBranch.getComplexValue(this.name,
-                  Components.interfaces.nsIPrefLocalizedString).data;
-        }
-        return value;
-
-      default:
-        throw new Error('Invalid value for ' + this.name);
+  {
+    item: 'converter',
+    from: 'number',
+    to: 'dom',
+    exec: nodeFromDataToString
+  },
+  {
+    item: 'converter',
+    from: 'boolean',
+    to: 'dom',
+    exec: nodeFromDataToString
+  },
+  {
+    item: 'converter',
+    from: 'undefined',
+    to: 'dom',
+    exec: function(data, conversionContext) {
+      return util.createElement(conversionContext.document, 'span');
     }
   },
-
-  set: function(value) {
-    if (imports.prefBranch.prefIsLocked(this.name)) {
-      throw new Error('Locked preference ' + this.name);
-    }
-
-    switch (imports.prefBranch.getPrefType(this.name)) {
-      case imports.prefBranch.PREF_BOOL:
-        imports.prefBranch.setBoolPref(this.name, value);
-        break;
-
-      case imports.prefBranch.PREF_INT:
-        imports.prefBranch.setIntPref(this.name, value);
-        break;
-
-      case imports.prefBranch.PREF_STRING:
-        imports.supportsString.data = value;
-        imports.prefBranch.setComplexValue(this.name,
-                Components.interfaces.nsISupportsString,
-                imports.supportsString);
-        break;
-
-      default:
-        throw new Error('Invalid value for ' + this.name);
-    }
-
-    Services.prefs.savePrefFile(null);
+  {
+    item: 'converter',
+    from: 'error',
+    to: 'dom',
+    exec: function(ex, conversionContext) {
+      var node = util.createElement(conversionContext.document, 'p');
+      node.className = "gcli-error";
+      node.textContent = ex;
+      return node;
+    }
+  }
+];
+
+});
+/*
+ * 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/terminal', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) {
+
+'use strict';
+
+var util = require('util/util');
+
+/**
+ * A 'terminal' object is a string or an array of strings, which are typically
+ * the output from a shell command
+ */
+exports.items = [
+  {
+    item: 'converter',
+    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, conversionContext) {
+      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);
+    }
   },
-
-  enumerable: true
-});
-
-/**
- * Reset this setting to it's initial default value
- */
-Setting.prototype.setDefault = function() {
-  imports.prefBranch.clearUserPref(this.name);
-  Services.prefs.savePrefFile(null);
-};
-
-
-/**
- * Collection of preferences for sorted access
- */
-var settingsAll = [];
-
-/**
- * Collection of preferences for fast indexed access
- */
-var settingsMap = new Map();
-
-/**
- * Flag so we know if we've read the system preferences
- */
-var hasReadSystem = false;
-
-/**
- * Clear out all preferences and return to initial state
- */
-function reset() {
-  settingsMap = new Map();
-  settingsAll = [];
-  hasReadSystem = false;
-}
-
-/**
- * Reset everything on startup and shutdown because we're doing lazy loading
- */
-exports.startup = function() {
-  reset();
-};
-
-exports.shutdown = function() {
-  reset();
-};
-
-/**
- * Load system prefs if they've not been loaded already
- * @return true
- */
-function readSystem() {
-  if (hasReadSystem) {
-    return;
-  }
-
-  imports.prefBranch.getChildList('').forEach(function(name) {
-    var setting = new Setting(name);
-    settingsAll.push(setting);
-    settingsMap.set(name, setting);
-  });
-
-  settingsAll.sort(function(s1, s2) {
-    return s1.name.localeCompare(s2.name);
-  });
-
-  hasReadSystem = true;
-}
-
-/**
- * Get an array containing all known Settings filtered to match the given
- * filter (string) at any point in the name of the setting
- */
-exports.getAll = function(filter) {
-  readSystem();
-
-  if (filter == null) {
-    return settingsAll;
-  }
-
-  return settingsAll.filter(function(setting) {
-    return setting.name.indexOf(filter) !== -1;
-  });
-};
-
-/**
- * Add a new setting.
- */
-exports.addSetting = function(prefSpec) {
-  var setting = new Setting(prefSpec);
-
-  if (settingsMap.has(setting.name)) {
-    // Once exists already, we're going to need to replace it in the array
-    for (var i = 0; i < settingsAll.length; i++) {
-      if (settingsAll[i].name === setting.name) {
-        settingsAll[i] = setting;
-      }
-    }
-  }
-
-  settingsMap.set(setting.name, setting);
-  exports.onChange({ added: setting.name });
-
-  return setting;
-};
-
-/**
- * Getter for an existing setting. Generally use of this function should be
- * avoided. Systems that define a setting should export it if they wish it to
- * be available to the outside, or not otherwise. Use of this function breaks
- * that boundary and also hides dependencies. Acceptable uses include testing
- * and embedded uses of GCLI that pre-define all settings (e.g. Firefox)
- * @param name The name of the setting to fetch
- * @return The found Setting object, or undefined if the setting was not found
- */
-exports.getSetting = function(name) {
-  // We might be able to give the answer without needing to read all system
-  // settings if this is an internal setting
-  var found = settingsMap.get(name);
-  if (found) {
-    return found;
-  }
-
-  if (hasReadSystem) {
-    return undefined;
-  }
-  else {
-    readSystem();
-    return settingsMap.get(name);
-  }
-};
-
-/**
- * Event for use to detect when the list of settings changes
- */
-exports.onChange = util.createEvent('Settings.onChange');
-
-/**
- * Remove a setting. A no-op in this case
- */
-exports.removeSetting = function() { };
+  {
+    item: 'converter',
+    from: 'terminal',
+    to: 'string',
+    exec: function(data, conversionContext) {
+      return Array.isArray(data) ? data.join('') : '' + data;
+    }
+  }
+];
 
 
 });
 /*
  * 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.
@@ -5657,70 +6999,58 @@ define('gcli/ui/intro', ['require', 'exp
 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!'
  */
-var hideIntroSettingSpec = {
-  name: 'hideIntro',
-  type: 'boolean',
-  description: l10n.lookup('hideIntroDesc'),
-  defaultValue: false
-};
-var hideIntro;
-
-/**
- * Register (and unregister) the hide-intro setting
- */
-exports.startup = function() {
-  hideIntro = settings.addSetting(hideIntroSettingSpec);
-};
-
-exports.shutdown = function() {
-  settings.removeSetting(hideIntroSettingSpec);
-  hideIntro = undefined;
-};
+exports.items = [
+  {
+    item: 'setting',
+    name: 'hideIntro',
+    type: 'boolean',
+    description: l10n.lookup('hideIntroDesc'),
+    defaultValue: false
+  }
+];
 
 /**
  * Called when the UI is ready to add a welcome message to the output
  */
 exports.maybeShowIntro = function(commandOutputManager, conversionContext) {
+  var hideIntro = settings.getSetting('hideIntro');
   if (hideIntro.value) {
     return;
   }
 
   var output = new Output();
   output.type = 'view';
   commandOutputManager.onOutput({ output: output });
 
-  var viewData = this.createView(conversionContext, output);
+  var viewData = this.createView(null, 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(conversionContext, output) {
+exports.createView = function(ignore, conversionContext, output) {
   return view.createView({
     html: require('text!gcli/ui/intro.html'),
     options: { stack: 'intro.html' },
     data: {
       l10n: l10n.propertyLookup,
-      onclick: function(ev) {
-        conversionContext.update(ev.currentTarget);
-      },
-      ondblclick: function(ev) {
-        conversionContext.updateExec(ev.currentTarget);
-      },
+      onclick: conversionContext.update,
+      ondblclick: conversionContext.updateExec,
       showHideButton: (output != null),
       onGotIt: function(ev) {
+        var hideIntro = settings.getSetting('hideIntro');
         hideIntro.value = true;
         output.onClose();
       }
     }
   });
 };
 
 });
@@ -5873,30 +7203,25 @@ var Conversion = require('gcli/types').C
 
 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;
 
-var evalCommand;
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  evalCommand = canon.addCommand(evalCommandSpec);
-};
-
-exports.shutdown = function() {
-  canon.removeCommand(evalCommandSpec.name);
-  evalCommand = undefined;
-};
-
+/**
+ * Some manual intervention is needed in parsing the { command.
+ */
+function getEvalCommand() {
+  if (getEvalCommand._cmd == null) {
+    getEvalCommand._cmd = canon.getCommand(evalCmd.name);
+  }
+  return getEvalCommand._cmd;
+}
 
 /**
  * Assignment is a link between a parameter and the data for that parameter.
  * The data for the parameter is available as in the preferred type and as
  * an Argument for the CLI.
  * <p>We also record validity information where applicable.
  * <p>For values, null and undefined have distinct definitions. null means
  * that a value has been provided, undefined means that it has not.
@@ -6090,34 +7415,36 @@ exports.setEvalFunction = function(newCu
  */
 exports.unsetEvalFunction = function() {
   customEval = undefined;
 };
 
 /**
  * 'eval' command
  */
-var evalCommandSpec = {
+var evalCmd = {
+  item: 'command',
   name: '{',
   params: [
     {
       name: 'javascript',
       type: 'javascript',
       description: ''
     }
   ],
   hidden: true,
   returnType: 'object',
   description: { key: 'cliEvalJavascript' },
   exec: function(args, context) {
     return customEval(args.javascript);
   },
-  evalRegexp: /^\s*{\s*/
-};
-
+  isCommandRegexp: /^\s*{\s*/
+};
+
+exports.items = [ evalCmd ];
 
 /**
  * This is a special assignment to reflect the command itself.
  */
 function CommandAssignment() {
   var commandParamMetadata = { name: '__command', type: 'command' };
   // This is a hack so that rather than reply with a generic description of the
   // command assignment, we reply with the description of the assigned command,
@@ -6164,17 +7491,17 @@ function UnassignedAssignment(requisitio
     }
   });
   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.executionContext);
   this.conversion = util.synchronize(parsed);
-  this.conversion.assign(this);
+  this.conversion.assignment = this;
 }
 
 UnassignedAssignment.prototype = Object.create(Assignment.prototype);
 
 UnassignedAssignment.prototype.getStatus = function(arg) {
   return this.conversion.getStatus();
 };
 
@@ -6225,19 +7552,19 @@ function Requisition(environment, doc, c
   this.shell = {
     cwd: '/', // Where we store the current working directory
     env: {}   // Where we store the current environment
   };
 
   // The command that we are about to execute.
   // @see setCommandConversion()
   this.commandAssignment = new CommandAssignment();
-  var promise = this.setAssignment(this.commandAssignment, null,
-                                   { skipArgUpdate: true });
-  util.synchronize(promise);
+  var assignPromise = this.setAssignment(this.commandAssignment, null,
+                                   { internal: true });
+  util.synchronize(assignPromise);
 
   // The object that stores of Assignment objects that we are filling out.
   // The Assignment objects are stored under their param.name for named
   // lookup. Note: We make use of the property of Javascript objects that
   // they are not just hashmaps, but linked-list hashmaps which iterate in
   // insertion order.
   // _assignments excludes the commandAssignment.
   this._assignments = {};
@@ -6250,16 +7577,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;
 
+  // Changes can be asynchronous, when one update starts before another
+  // finishes we abandon the former change
+  this._nextUpdateId = 0;
+
   // 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');
@@ -6272,16 +7603,55 @@ 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;
 };
 
+/**
+ * If we're about to make an asynchronous change when other async changes could
+ * overtake this one, then we want to be able to bail out if overtaken. The
+ * value passed back from beginChange should be passed to endChangeCheckOrder
+ * on completion of calculation, before the results are applied in order to
+ * check that the calculation has not been overtaken
+ */
+Requisition.prototype._beginChange = function() {
+  this._structuralChangeInProgress = true;
+  var updateId = this._nextUpdateId;
+  this._nextUpdateId++;
+  return updateId;
+};
+
+/**
+ * Check to see if another change has started since updateId started.
+ * This allows us to bail out of an update.
+ * It's hard to make updates atomic because until you've responded to a parse
+ * of the command argument, you don't know how to parse the arguments to that
+ * command.
+ */
+Requisition.prototype._isChangeCurrent = function(updateId) {
+  return updateId + 1 === this._nextUpdateId;
+};
+
+/**
+ * See notes on beginChange
+ */
+Requisition.prototype._endChangeCheckOrder = function(updateId) {
+  if (updateId + 1 !== this._nextUpdateId) {
+    // An update that started after we did has already finished, so our
+    // changes are out of date. Abandon further work.
+    return false;
+  }
+
+  this._structuralChangeInProgress = false;
+  return true;
+};
+
 var legacy = false;
 
 /**
  * Functions and data related to the execution of a command
  */
 Object.defineProperty(Requisition.prototype, 'executionContext', {
   get: function() {
     if (this._executionContext == null) {
@@ -6424,19 +7794,18 @@ Requisition.prototype._commandAssignment
 
   this._assignments = {};
 
   var command = this.commandAssignment.value;
   if (command) {
     for (var i = 0; i < command.params.length; i++) {
       var param = command.params[i];
       var assignment = new Assignment(param, i);
-      var promise = this.setAssignment(assignment, null,
-                                       { skipArgUpdate: true });
-      util.synchronize(promise);
+      var assignPromise = this.setAssignment(assignment, null, { internal: true });
+      util.synchronize(assignPromise);
       assignment.onAssignmentChange.add(this._assignmentChanged, this);
       this._assignments[param.name] = assignment;
     }
   }
   this.assignmentCount = Object.keys(this._assignments).length;
 };
 
 /**
@@ -6518,16 +7887,38 @@ Requisition.prototype.getStatus = functi
   }, this);
   if (status === Status.INCOMPLETE) {
     status = Status.ERROR;
   }
   return status;
 };
 
 /**
+ * If ``requisition.getStatus() != VALID`` message then return a string which
+ * best describes what is wrong. Generally error messages are delivered by
+ * looking at the error associated with the argument at the cursor, but there
+ * are times when you just want to say 'tell me the worst'.
+ * If ``requisition.getStatus() != VALID`` then return ``null``.
+ */
+Requisition.prototype.getStatusMessage = function() {
+  var message = null;
+  this.getAssignments(true).forEach(function(assignment) {
+    if (assignment.getStatus() !== Status.VALID) {
+      message = assignment.getMessage();
+    }
+  }, this);
+
+  if (message == null && this._unassigned.length !== 0) {
+    message = l10n.lookup('cliUnusedArg');
+  }
+
+  return message;
+};
+
+/**
  * Extract the names and values of all the assignments, and return as
  * an object.
  */
 Requisition.prototype.getArgsObject = function() {
   var args = {};
   this.getAssignments().forEach(function(assignment) {
     args[assignment.param.name] = assignment.conversion.isDataProvided() ?
             assignment.value :
@@ -6555,27 +7946,29 @@ Requisition.prototype.getAssignments = f
 
 /**
  * Internal function to alter the given assignment using the given arg.
  * @param assignment The assignment to alter
  * @param arg The new value for the assignment. An instance of Argument, or an
  * instance of Conversion, or null to set the blank value.
  * @param options There are a number of ways to customize how the assignment
  * is made, including:
- * - skipArgUpdate: (default:false) Adjusts the args in this requisition to keep
- *   things up to date. Args should only be skipped when setAssignment is being
- *   called as part of the update process.
- * - matchPadding: (default:false) Altering the whitespace on the prefix and
+ * - internal: (default:false) External updates are required to do more work,
+ *   including adjusting the args in this requisition to stay in sync.
+ *   On the other hand non internal changes use beginChange to back out of
+ *   changes when overtaken asynchronously.
+ *   Setting internal:true effectively means this is being called as part of
+ *   the update process.
+ * - matchPadding: (default:false) Alter the whitespace on the prefix and
  *   suffix of the new argument to match that of the old argument. This only
- *   makes sense with skipArgUpdate=false
- *   then further take the step of
+ *   makes sense with internal=false
  */
 Requisition.prototype.setAssignment = function(assignment, arg, options) {
   options = options || {};
-  if (options.skipArgUpdate !== true) {
+  if (!options.internal) {
     var originalArgs = assignment.arg.getArgs();
 
     // Update the args array
     var replacementArgs = arg.getArgs();
     var maxLen = Math.max(originalArgs.length, replacementArgs.length);
     for (var i = 0; i < maxLen; i++) {
       // If there are no more original args, or if the original arg was blank
       // (i.e. not typed by the user), we'll just need to add at the end
@@ -6606,57 +7999,58 @@ Requisition.prototype.setAssignment = fu
             replacementArgs[i].suffix = this._args[index].suffix;
           }
         }
         this._args[index] = replacementArgs[i];
       }
     }
   }
 
-  function setAssignmentInternal(conversion) {
-    var oldConversion = assignment.conversion;
-
-    assignment.conversion = conversion;
-    assignment.conversion.assign(assignment);
-
-    if (assignment.conversion.equals(oldConversion)) {
-      return;
-    }
-
-    assignment.onAssignmentChange({
-      assignment: assignment,
-      conversion: assignment.conversion,
-      oldConversion: oldConversion
-    });
-  }
+  var updateId = options.internal ? null : this._beginChange();
+
+  var setAssignmentInternal = function(conversion) {
+    if (options.internal || this._endChangeCheckOrder(updateId)) {
+      var oldConversion = assignment.conversion;
+
+      assignment.conversion = conversion;
+      assignment.conversion.assignment = assignment;
+
+      if (!assignment.conversion.equals(oldConversion)) {
+        assignment.onAssignmentChange({
+          assignment: assignment,
+          conversion: assignment.conversion,
+          oldConversion: oldConversion
+        });
+      }
+    }
+
+    return promise.resolve(undefined);
+  }.bind(this);
 
   if (arg == null) {
     var blank = assignment.param.type.getBlank(this.executionContext);
-    setAssignmentInternal(blank);
-  }
-  else if (typeof arg.getStatus === 'function') {
-    setAssignmentInternal(arg);
-  }
-  else {
-    var parsed = assignment.param.type.parse(arg, this.executionContext);
-    return parsed.then(function(conversion) {
-      setAssignmentInternal(conversion);
-    }.bind(this));
-  }
-
-  return promise.resolve(undefined);
+    return setAssignmentInternal(blank);
+  }
+
+  if (typeof arg.getStatus === 'function') {
+    // It's not really an arg, it's a conversion already
+    return setAssignmentInternal(arg);
+  }
+
+  var parsed = assignment.param.type.parse(arg, this.executionContext);
+  return parsed.then(setAssignmentInternal);
 };
 
 /**
  * Reset all the assignments to their default values
  */
 Requisition.prototype.setBlankArguments = function() {
   this.getAssignments().forEach(function(assignment) {
-    var promise = this.setAssignment(assignment, null, { skipArgUpdate: true });
-    util.synchronize(promise);
+    var assignPromise = this.setAssignment(assignment, null, { internal: true });
+    util.synchronize(assignPromise);
   }, this);
 };
 
 /**
  * Complete the argument at <tt>cursor</tt>.
  * Basically the same as:
  *   assignment = getAssignmentAt(cursor);
  *   assignment.value = assignment.conversion.predictions[0];
@@ -6707,36 +8101,37 @@ Requisition.prototype.complete = functio
       }
     }
     else {
       // Mutate this argument to hold the completion
       var arg = assignment.arg.beget({
         text: prediction.name,
         dontQuote: (assignment === this.commandAssignment)
       });
-      var assignmentPromise = this.setAssignment(assignment, arg);
+      var assignPromise = this.setAssignment(assignment, arg);
 
       if (!prediction.incomplete) {
-        assignmentPromise = assignmentPromise.then(function() {
+        assignPromise = assignPromise.then(function() {
           // The prediction is complete, add a space to let the user move-on
           return this._addSpace(assignment).then(function() {
             // Bug 779443 - Remove or explain the re-parse
             if (assignment instanceof UnassignedAssignment) {
               return this.update(this.toString());
             }
           }.bind(this));
         }.bind(this));
       }
 
-      outstanding.push(assignmentPromise);
+      outstanding.push(assignPromise);
     }
 
     return promise.all(outstanding).then(function() {
       this.onTextChange();
       this.onTextChange.resumeFire();
+      return true;
     }.bind(this));
   }.bind(this));
 };
 
 /**
  * A test method to check that all args are assigned in some way
  */
 Requisition.prototype._assertArgsAssigned = function() {
@@ -6767,33 +8162,33 @@ Requisition.prototype._addSpace = functi
  */
 Requisition.prototype.decrement = function(assignment) {
   var replacement = assignment.param.type.decrement(assignment.conversion.value,
                                                     this.executionContext);
   if (replacement != null) {
     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);
+    var assignPromise = this.setAssignment(assignment, arg);
+    util.synchronize(assignPromise);
   }
 };
 
 /**
  * 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.executionContext);
   if (replacement != null) {
     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);
+    var assignPromise = this.setAssignment(assignment, arg);
+    util.synchronize(assignPromise);
   }
 };
 
 /**
  * Extract a canonical version of the input
  */
 Requisition.prototype.toCanonicalString = function() {
   var line = [];
@@ -6831,24 +8226,24 @@ 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
- * to Assignments by #_assign() (i.e. they are not instances of NamedArgument,
+ * 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.
+ * See _assign() for details.
  */
 Requisition.prototype.createInputArgTrace = function() {
   if (!this._args) {
     throw new Error('createInputMap requires a command line. See source.');
     // If this is a problem then we can fake command line input using
     // something like the code in #toCanonicalString().
   }
 
@@ -7080,18 +8475,18 @@ Requisition.prototype.exec = function(op
   }
 
   if (!command) {
     throw new Error('Unknown command');
   }
 
   // Display JavaScript input without the initial { or closing }
   var typed = this.toString();
-  if (evalCommandSpec.evalRegexp.test(typed)) {
-    typed = typed.replace(evalCommandSpec.evalRegexp, '');
+  if (evalCmd.isCommandRegexp.test(typed)) {
+    typed = typed.replace(evalCmd.isCommandRegexp, '');
     // Bug 717763: What if the JavaScript naturally ends with a }?
     typed = typed.replace(/\s*}\s*$/, '');
   }
 
   var output = new Output({
     command: command,
     args: args,
     typed: typed,
@@ -7147,20 +8542,20 @@ 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 promise = commandType.parse(arg, this.executionContext);
+  var parsePromise = commandType.parse(arg, this.executionContext);
   this.setAssignment(this.commandAssignment,
-                     util.synchronize(promise),
-                     { skipArgUpdate: true });
+                     util.synchronize(parsePromise),
+                     { internal: true });
 
   this._structuralChangeInProgress = false;
   this.onTextChange();
 };
 
 /**
  * Helper to find the 'data-command' attribute, used by |update()|
  */
@@ -7180,25 +8575,33 @@ function getDataCommandAttribute(element
 Requisition.prototype.update = function(typed) {
   if (typeof HTMLElement !== 'undefined' && typed instanceof HTMLElement) {
     typed = getDataCommandAttribute(typed);
   }
   if (typeof Event !== 'undefined' && typed instanceof Event) {
     typed = getDataCommandAttribute(typed.currentTarget);
   }
 
-  this._structuralChangeInProgress = true;
+  var updateId = this._beginChange();
 
   this._args = exports.tokenize(typed);
   var args = this._args.slice(0); // i.e. clone
 
   return this._split(args).then(function() {
+    if (!this._isChangeCurrent(updateId)) {
+      return false;
+    }
+
     return this._assign(args).then(function() {
-      this._structuralChangeInProgress = false;
-      this.onTextChange();
+      if (this._endChangeCheckOrder(updateId)) {
+        this.onTextChange();
+        return true;
+      }
+
+      return false;
     }.bind(this));
   }.bind(this));
 };
 
 /**
  * For test/debug use only. The output from this function is subject to wanton
  * random change without notice, and should not be relied upon to even exist
  * at some later date.
@@ -7451,42 +8854,42 @@ function isSimple(typed) {
 
 /**
  * Looks in the canon for a command extension that matches what has been
  * typed at the command line.
  */
 Requisition.prototype._split = function(args) {
   // We're processing args, so we don't want the assignments that we make to
   // try to adjust other args assuming this is an external update
-  var noArgUp = { skipArgUpdate: true };
+  var noArgUp = { internal: true };
 
   // Handle the special case of the user typing { javascript(); }
   // We use the hidden 'eval' command directly rather than shift()ing one of
   // the parameters, and parse()ing it.
   var conversion = undefined;
   if (args[0].type === 'ScriptArgument') {
     // Special case: if the user enters { console.log('foo'); } then we need to
     // use the hidden 'eval' command
-    conversion = new Conversion(evalCommand, new ScriptArgument());
+    conversion = new Conversion(getEvalCommand(), new ScriptArgument());
     return this.setAssignment(this.commandAssignment, conversion, noArgUp);
   }
 
   var argsUsed = 1;
 
   var parsePromise;
   var commandType = this.commandAssignment.param.type;
   while (argsUsed <= args.length) {
     var arg = (argsUsed === 1) ?
               args[0] :
               new MergedArgument(args, 0, argsUsed);
 
     // Making the commandType.parse() promise as synchronous is OK because we
     // know that commandType is a synchronous type.
 
-    if (this.prefix != nul