Bug 767587 - GCLI should have a type for files; r=mratcliffe
authorJoe Walker <jwalker@mozilla.com>
Sat, 20 Jul 2013 05:17:24 +0100
changeset 151606 0ebbdfbb31ebe78e62fd950f9e485b87d886d43b
parent 151605 bc59f7e483c5e3404b33a8b83b8506908e1edf40
child 151607 7c3d5bdc569c47fe3e61a2cdcb09331b344225c2
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)
reviewersmratcliffe
bugs767587
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 767587 - GCLI should have a type for files; r=mratcliffe
browser/devtools/commandline/BuiltinCommands.jsm
browser/devtools/commandline/test/Makefile.in
browser/devtools/commandline/test/browser_gcli_async.js
browser/devtools/commandline/test/browser_gcli_canon.js
browser/devtools/commandline/test/browser_gcli_context.js
browser/devtools/commandline/test/browser_gcli_date.js
browser/devtools/commandline/test/browser_gcli_fail.js
browser/devtools/commandline/test/browser_gcli_file.js
browser/devtools/commandline/test/browser_gcli_fileparser.js
browser/devtools/commandline/test/browser_gcli_filesystem.js
browser/devtools/commandline/test/browser_gcli_focus.js
browser/devtools/commandline/test/browser_gcli_incomplete.js
browser/devtools/commandline/test/browser_gcli_menu.js
browser/devtools/commandline/test/browser_gcli_node.js
browser/devtools/commandline/test/browser_gcli_remote.js
browser/devtools/commandline/test/browser_gcli_spell.js
browser/devtools/commandline/test/browser_gcli_string.js
browser/devtools/commandline/test/helpers.js
browser/devtools/commandline/test/mockCommands.js
browser/locales/en-US/chrome/browser/devtools/gcli.properties
browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
toolkit/devtools/gcli/gcli.jsm
--- 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/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -42,16 +42,40 @@ cliEvalJavascript=Enter JavaScript direc
 # than the current command can understand this is the error message shown to
 # the user.
 cliUnusedArg=Too many arguments
 
 # LOCALIZATION NOTE (cliOptions): The title of the dialog which displays the
 # options that are available to the current command.
 cliOptions=Available Options
 
+# LOCALIZATION NOTE (fileErrNotExists): 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 (fileErrExists): 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 (fileErrIsNotFile): 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 (fileErrIsNotDirectory): 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 (fileErrDoesntMatch): 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 (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.
 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
--- 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/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 != null && this.prefix != '') {
+    if (this.prefix != null && this.prefix !== '') {
       var prefixArg = new Argument(this.prefix, '', ' ');
       var prefixedArg = new MergedArgument([ prefixArg, arg ]);
 
       parsePromise = commandType.parse(prefixedArg, this.executionContext);
       conversion = util.synchronize(parsePromise);
 
       if (conversion.value == null) {
         parsePromise = commandType.parse(arg, this.executionContext);
@@ -7534,17 +8937,17 @@ Requisition.prototype._addUnassignedArgs
   }.bind(this));
 };
 
 /**
  * Work out which arguments are applicable to which parameters.
  */
 Requisition.prototype._assign = function(args) {
   // See comment in _split. Avoid multiple updates
-  var noArgUp = { skipArgUpdate: true };
+  var noArgUp = { internal: true };
 
   this._unassigned = [];
   var outstanding = [];
 
   if (!this.commandAssignment.value) {
     this._addUnassignedArgs(args);
     return promise.all(outstanding);
   }
@@ -7638,46 +9041,48 @@ Requisition.prototype._assign = function
     if (assignment.param.type.name === 'array') {
       var arrayArg = arrayArgs[assignment.param.name];
       if (!arrayArg) {
         arrayArg = new ArrayArgument();
         arrayArgs[assignment.param.name] = arrayArg;
       }
       arrayArg.addArguments(args);
       args = [];
+      // The actual assignment to the array parameter is done below
+      return;
+    }
+
+    // Set assignment to defaults if there are no more arguments
+    if (args.length === 0) {
+      outstanding.push(this.setAssignment(assignment, null, noArgUp));
+      return;
+    }
+
+    var arg = args.splice(0, 1)[0];
+    // --foo and -f are named parameters, -4 is a number. So '-' is either
+    // the start of a named parameter or a number depending on the context
+    var isIncompleteName = assignment.param.type.name === 'number' ?
+        /-[-a-zA-Z_]/.test(arg.text) :
+        arg.text.charAt(0) === '-';
+
+    if (isIncompleteName) {
+      this._unassigned.push(new UnassignedAssignment(this, arg));
     }
     else {
-      if (args.length === 0) {
-        outstanding.push(this.setAssignment(assignment, null, noArgUp));
-      }
-      else {
-        var arg = args.splice(0, 1)[0];
-        // --foo and -f are named parameters, -4 is a number. So '-' is either
-        // the start of a named parameter or a number depending on the context
-        var isIncompleteName = assignment.param.type.name === 'number' ?
-            /-[-a-zA-Z_]/.test(arg.text) :
-            arg.text.charAt(0) === '-';
-
-        if (isIncompleteName) {
-          this._unassigned.push(new UnassignedAssignment(this, arg));
-        }
-        else {
-          outstanding.push(this.setAssignment(assignment, arg, noArgUp));
-        }
-      }
+      outstanding.push(this.setAssignment(assignment, arg, noArgUp));
     }
   }, this);
 
   // Now we need to assign the array argument (if any)
   Object.keys(arrayArgs).forEach(function(name) {
     var assignment = this.getAssignment(name);
     outstanding.push(this.setAssignment(assignment, arrayArgs[name], noArgUp));
   }, this);
 
-  // What's left is can't be assigned, but we need to extract
+  // What's left is can't be assigned, but we need to officially unassign them
   this._addUnassignedArgs(args);
 
   return promise.all(outstanding);
 };
 
 exports.Requisition = Requisition;
 
 /**
@@ -7767,60 +9172,53 @@ define("text!gcli/ui/intro.html", [], "\
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/focus', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/settings', 'gcli/canon'], function(require, exports, module) {
+define('gcli/ui/focus', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/settings'], function(require, exports, module) {
 
 'use strict';
 
 var util = require('util/util');
 var l10n = require('util/l10n');
 var settings = require('gcli/settings');
-var canon = require('gcli/canon');
 
 /**
  * Record how much help the user wants from the tooltip
  */
 var Eagerness = {
   NEVER: 1,
   SOMETIMES: 2,
   ALWAYS: 3
 };
-var eagerHelperSettingSpec = {
-  name: 'eagerHelper',
-  type: {
-    name: 'selection',
-    lookup: [
-      { name: 'never', value: Eagerness.NEVER },
-      { name: 'sometimes', value: Eagerness.SOMETIMES },
-      { name: 'always', value: Eagerness.ALWAYS }
-    ]
-  },
-  defaultValue: Eagerness.SOMETIMES,
-  description: l10n.lookup('eagerHelperDesc'),
-  ignoreTypeDifference: true
-};
-var eagerHelper;
-
-/**
- * Register (and unregister) the hide-intro setting
- */
-exports.startup = function() {
-  eagerHelper = settings.addSetting(eagerHelperSettingSpec);
-};
-
-exports.shutdown = function() {
-  settings.removeSetting(eagerHelperSettingSpec);
-  eagerHelper = undefined;
-};
+
+/**
+ * Export the eagerHelper setting
+ */
+exports.items = [
+  {
+    item: 'setting',
+    name: 'eagerHelper',
+    type: {
+      name: 'selection',
+      lookup: [
+        { name: 'never', value: Eagerness.NEVER },
+        { name: 'sometimes', value: Eagerness.SOMETIMES },
+        { name: 'always', value: Eagerness.ALWAYS }
+      ]
+    },
+    defaultValue: Eagerness.SOMETIMES,
+    description: l10n.lookup('eagerHelperDesc'),
+    ignoreTypeDifference: true
+  }
+];
 
 /**
  * FocusManager solves the problem of tracking focus among a set of nodes.
  * The specific problem we are solving is when the hint element must be visible
  * if either the command line or any of the inputs in the hint element has the
  * focus, and invisible at other times, without hiding and showing the hint
  * element even briefly as the focus changes between them.
  * It does this simply by postponing the hide events by 250ms to see if
@@ -7852,27 +9250,29 @@ function FocusManager(options, component
   this._helpRequested = false;
   this._recentOutput = false;
 
   this.onVisibilityChange = util.createEvent('FocusManager.onVisibilityChange');
 
   this._focused = this._focused.bind(this);
   this._document.addEventListener('focus', this._focused, true);
 
+  var eagerHelper = settings.getSetting('eagerHelper');
   eagerHelper.onChange.add(this._eagerHelperChanged, this);
 
   this.isTooltipVisible = undefined;
   this.isOutputVisible = undefined;
   this._checkShow();
 }
 
 /**
  * Avoid memory leaks
  */
 FocusManager.prototype.destroy = function() {
+  var eagerHelper = settings.getSetting('eagerHelper');
   eagerHelper.onChange.remove(this._eagerHelperChanged, this);
 
   this._document.removeEventListener('focus', this._focused, true);
   this._requisition.commandOutputManager.onOutput.remove(this._outputted, this);
 
   for (var i = 0; i < this._monitoredElements.length; i++) {
     var monitor = this._monitoredElements[i];
     console.error('Hanging monitored element: ', monitor.element);
@@ -8129,16 +9529,17 @@ FocusManager.prototype._checkShow = func
  * Calculate if we should be showing or hidden taking into account all the
  * available inputs
  */
 FocusManager.prototype._shouldShowTooltip = function() {
   if (!this._hasFocus) {
     return { visible: false, reason: 'notHasFocus' };
   }
 
+  var eagerHelper = settings.getSetting('eagerHelper');
   if (eagerHelper.value === Eagerness.NEVER) {
     return { visible: false, reason: 'eagerHelperNever' };
   }
 
   if (eagerHelper.value === Eagerness.ALWAYS) {
     return { visible: true, reason: 'eagerHelperAlways' };
   }
 
@@ -8206,36 +9607,16 @@ var FalseNamedArgument = require('gcli/a
 var ArrayArgument = require('gcli/argument').ArrayArgument;
 var ArrayConversion = require('gcli/types').ArrayConversion;
 
 var Field = require('gcli/ui/fields').Field;
 var fields = require('gcli/ui/fields');
 
 
 /**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  fields.addField(StringField);
-  fields.addField(NumberField);
-  fields.addField(BooleanField);
-  fields.addField(DelegateField);
-  fields.addField(ArrayField);
-};
-
-exports.shutdown = function() {
-  fields.removeField(StringField);
-  fields.removeField(NumberField);
-  fields.removeField(BooleanField);
-  fields.removeField(DelegateField);
-  fields.removeField(ArrayField);
-};
-
-
-/**
  * A field that allows editing of strings
  */
 function StringField(type, options) {
   Field.call(this, type, options);
   this.arg = new Argument();
 
   this.element = util.createElement(this.document, 'input');
   this.element.type = 'text';
@@ -8493,34 +9874,34 @@ ArrayField.prototype.setConversion = fun
     this._onAdd(null, subConversion);
   }, this);
 };
 
 ArrayField.prototype.getConversion = function() {
   var conversions = [];
   var arrayArg = new ArrayArgument();
   for (var i = 0; i < this.members.length; i++) {
-    promise.resolve(this.members[i].field.getConversion()).then(function(conversion) {
+    Promise.resolve(this.members[i].field.getConversion()).then(function(conversion) {
       conversions.push(conversion);
       arrayArg.addArgument(conversion.arg);
     }.bind(this), util.errorHandler);
   }
   return new ArrayConversion(conversions, arrayArg);
 };
 
 ArrayField.prototype._onAdd = function(ev, subConversion) {
   // <div class=gcliArrayMbr save="${element}">
   var element = util.createElement(this.document, 'div');
   element.classList.add('gcli-array-member');
   this.container.appendChild(element);
 
   // ${field.element}
   var field = fields.getField(this.type.subtype, this.options);
   field.onFieldChange.add(function() {
-    promise.resolve(this.getConversion()).then(function(conversion) {
+    Promise.resolve(this.getConversion()).then(function(conversion) {
       this.onFieldChange({ conversion: conversion });
       this.setMessage(conversion.message);
     }.bind(this), util.errorHandler);
   }, this);
 
   if (subConversion) {
     field.setConversion(subConversion);
   }
@@ -8545,264 +9926,22 @@ ArrayField.prototype._onAdd = function(e
     });
     this.parent.onInputChange();
   }.bind(member);
   delButton.addEventListener('click', member.onDelete, false);
 
   this.members.push(member);
 };
 
-
-});
-/*
- * 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;
-}
-
-/**
- * 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);
+/**
+ * Exported items
+ */
+exports.items = [
+  StringField, NumberField, BooleanField, DelegateField, ArrayField
+];
 
 
 });
 /*
  * 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.
@@ -8825,30 +9964,16 @@ var util = require('util/util');
 var promise = require('util/promise');
 
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
 var ScriptArgument = require('gcli/argument').ScriptArgument;
 
 var Menu = require('gcli/ui/fields/menu').Menu;
 var Field = require('gcli/ui/fields').Field;
-var fields = require('gcli/ui/fields');
-
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  fields.addField(JavascriptField);
-};
-
-exports.shutdown = function() {
-  fields.removeField(JavascriptField);
-};
-
 
 /**
  * A field that allows editing of javascript
  */
 function JavascriptField(type, options) {
   Field.call(this, type, options);
 
   this.onInputChange = this.onInputChange.bind(this);
@@ -8948,16 +10073,21 @@ JavascriptField.prototype.onInputChange 
 JavascriptField.prototype.getConversion = function() {
   // This tweaks the prefix/suffix of the argument to fit
   this.arg = new ScriptArgument(this.input.value, '{ ', ' }');
   return this.type.parse(this.arg, this.requisition.executionContext);
 };
 
 JavascriptField.DEFAULT_VALUE = '__JavascriptField.DEFAULT_VALUE';
 
+/**
+ * Allow registration and de-registration.
+ */
+exports.items = [ JavascriptField ];
+
 
 });
 /*
  * 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
@@ -9209,45 +10339,28 @@ define("text!gcli/ui/fields/menu.html", 
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/ui/fields/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
+define('gcli/ui/fields/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/argument', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
 
 'use strict';
 
 var promise = require('util/promise');
 var util = require('util/util');
 var l10n = require('util/l10n');
 
 var Argument = require('gcli/argument').Argument;
-var Status = require('gcli/types').Status;
-var Conversion = require('gcli/types').Conversion;
 
 var Menu = require('gcli/ui/fields/menu').Menu;
 var Field = require('gcli/ui/fields').Field;
-var fields = require('gcli/ui/fields');
-
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  fields.addField(SelectionField);
-  fields.addField(SelectionTooltipField);
-};
-
-exports.shutdown = function() {
-  fields.removeField(SelectionField);
-  fields.removeField(SelectionTooltipField);
-};
 
 
 /**
  * Model an instanceof SelectionType as a select input box.
  * <p>There are 3 slightly overlapping concepts to be aware of:
  * <ul>
  * <li>value: This is the (probably non-string) value, known as a value by the
  *   assignment
@@ -9262,17 +10375,17 @@ function SelectionField(type, options) {
   this.items = [];
 
   this.element = util.createElement(this.document, 'select');
   this.element.classList.add('gcli-field');
   this._addOption({
     name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ])
   });
 
-  promise.resolve(this.type.getLookup()).then(function(lookup) {
+  Promise.resolve(this.type.getLookup()).then(function(lookup) {
     lookup.forEach(this._addOption, this);
   }.bind(this), util.errorHandler);
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('change', this.onInputChange, false);
 
   this.onFieldChange = util.createEvent('SelectionField.onFieldChange');
 }
@@ -9360,17 +10473,19 @@ SelectionTooltipField.prototype.setConve
   this.arg = conversion.arg;
   this.setMessage(conversion.message);
 
   conversion.getPredictions().then(function(predictions) {
     var items = predictions.map(function(prediction) {
       // If the prediction value is an 'item' (that is an object with a name and
       // description) then use that, otherwise use the prediction itself, because
       // at least that has a name.
-      return prediction.value.description ? prediction.value : prediction;
+      return prediction.value && prediction.value.description ?
+          prediction.value :
+          prediction;
     }, this);
     this.menu.show(items, conversion.arg.text);
   }.bind(this), util.errorHandler);
 };
 
 SelectionTooltipField.prototype.itemClicked = function(ev) {
   var parsed = this.type.parse(ev.arg, this.requisition.executionContext);
   promise.resolve(parsed).then(function(conversion) {
@@ -9413,16 +10528,20 @@ Object.defineProperty(SelectionTooltipFi
   get: function() {
     return this.type.name !== 'command';
   },
   enumerable: true
 });
 
 SelectionTooltipField.DEFAULT_VALUE = '__SelectionTooltipField.DEFAULT_VALUE';
 
+/**
+ * Allow registration and de-registration.
+ */
+exports.items = [ SelectionField, SelectionTooltipField ];
 
 });
 /*
  * 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
@@ -9431,34 +10550,48 @@ SelectionTooltipField.DEFAULT_VALUE = '_
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/commands/connect', ['require', 'exports', 'module' , 'util/l10n', 'gcli/types', 'gcli/canon', 'util/connect/connector'], function(require, exports, module) {
+define('gcli/commands/connect', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon', 'util/connect/connector'], function(require, exports, module) {
 
 'use strict';
 
 var l10n = require('util/l10n');
-var types = require('gcli/types');
 var canon = require('gcli/canon');
 var connector = require('util/connect/connector');
 
 /**
  * A lookup of the current connection
  */
 var connections = {};
 
 /**
+ * 'connection' type
+ */
+var connection = {
+  item: 'type',
+  name: 'connection',
+  parent: 'selection',
+  lookup: function() {
+    return Object.keys(connections).map(function(prefix) {
+      return { name: prefix, value: connections[prefix] };
+    });
+  }
+};
+
+/**
  * 'connect' command
  */
 var connect = {
+  item: 'command',
   name: 'connect',
   description: l10n.lookup('connectDesc'),
   manual: l10n.lookup('connectManual'),
   params: [
     {
       name: 'prefix',
       type: 'string',
       description: l10n.lookup('connectPrefixDesc')
@@ -9523,32 +10656,20 @@ var connect = {
           throw typedData;
         }
       });
     }.bind(this);
   }
 };
 
 /**
- * 'connection' type
- */
-var connection = {
-  name: 'connection',
-  parent: 'selection',
-  lookup: function() {
-    return Object.keys(connections).map(function(prefix) {
-      return { name: prefix, value: connections[prefix] };
-    });
-  }
-};
-
-/**
  * 'disconnect' command
  */
 var disconnect = {
+  item: 'command',
   name: 'disconnect',
   description: l10n.lookup('disconnectDesc'),
   manual: l10n.lookup('disconnectManual'),
   params: [
     {
       name: 'prefix',
       type: 'connection',
       description: l10n.lookup('disconnectPrefixDesc'),
@@ -9567,34 +10688,17 @@ var disconnect = {
     return args.prefix.disconnect(args.force).then(function() {
       var removed = canon.removeProxyCommands(args.prefix.prefix);
       delete connections[args.prefix.prefix];
       return l10n.lookupFormat('disconnectReply', [ removed.length ]);
     });
   }
 };
 
-
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  types.addType(connection);
-
-  canon.addCommand(connect);
-  canon.addCommand(disconnect);
-};
-
-exports.shutdown = function() {
-  canon.removeCommand(connect);
-  canon.removeCommand(disconnect);
-
-  types.removeType(connection);
-};
-
+exports.items = [ connection, connect, disconnect ];
 
 });
 /*
  * 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
@@ -9778,27 +10882,27 @@ Request.prototype.complete = function(er
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/commands/context', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon'], function(require, exports, module) {
+define('gcli/commands/context', ['require', 'exports', 'module' , 'util/l10n'], function(require, exports, module) {
 
 'use strict';
 
 var l10n = require('util/l10n');
-var canon = require('gcli/canon');
 
 /**
  * 'context' command
  */
-var contextCmdSpec = {
+var context = {
+  item: 'command',
   name: 'context',
   description: l10n.lookup('contextDesc'),
   manual: l10n.lookup('contextManual'),
   params: [
    {
      name: 'prefix',
      type: 'command',
      description: l10n.lookup('contextPrefixDesc'),
@@ -9821,26 +10925,17 @@ var contextCmdSpec = {
                                         [ args.prefix.name ]));
     }
 
     requisition.prefix = args.prefix.name;
     return l10n.lookupFormat('contextReply', [ args.prefix.name ]);
   }
 };
 
-/**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  canon.addCommand(contextCmdSpec);
-};
-
-exports.shutdown = function() {
-  canon.removeCommand(contextCmdSpec);
-};
+exports.items = [ context ];
 
 });
 /*
  * 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
@@ -9849,32 +10944,32 @@ 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/commands/help', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon', 'gcli/converters', 'text!gcli/commands/help_man.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help.css'], function(require, exports, module) {
+define('gcli/commands/help', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon', 'text!gcli/commands/help_man.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help.css'], function(require, exports, module) {
 
 'use strict';
 
 var l10n = require('util/l10n');
 var canon = require('gcli/canon');
-var converters = require('gcli/converters');
 
 var helpManHtml = require('text!gcli/commands/help_man.html');
 var helpListHtml = require('text!gcli/commands/help_list.html');
 var helpCss = require('text!gcli/commands/help.css');
 
 /**
  * Convert a command into a man page
  */
-var commandConverterSpec = {
+var helpCommand = {
+  item: 'converter',
   from: 'commandData',
   to: 'view',
   exec: function(commandData, context) {
     return context.createView({
       html: helpManHtml,
       options: { allowEval: true, stack: 'help_man.html' },
       data: {
         l10n: l10n.propertyLookup,
@@ -9916,17 +11011,18 @@ var commandConverterSpec = {
       cssId: 'gcli-help'
     });
   }
 };
 
 /**
  * Convert a list of commands into a formatted list
  */
-var commandsConverterSpec = {
+var helpCommands = {
+  item: 'converter',
   from: 'commandsData',
   to: 'view',
   exec: function(commandsData, context) {
     var heading;
     if (commandsData.commands.length === 0) {
       heading = l10n.lookupFormat('helpListNone', [ commandsData.prefix ]);
     }
     else if (commandsData.prefix == null) {
@@ -9951,17 +11047,18 @@ var commandsConverterSpec = {
       cssId: 'gcli-help'
     });
   }
 };
 
 /**
  * 'help' command
  */
-var helpCommandSpec = {
+var help = {
+  item: 'command',
   name: 'help',
   description: l10n.lookup('helpDesc'),
   manual: l10n.lookup('helpManual'),
   params: [
     {
       name: 'search',
       type: 'string',
       description: l10n.lookup('helpSearchDesc'),
@@ -9982,31 +11079,16 @@ var helpCommandSpec = {
     return context.typedData('commandsData', {
       prefix: args.search,
       commands: getMatchingCommands(args.search)
     });
   }
 };
 
 /**
- * Registration and de-registration.
- */
-exports.startup = function() {
-  canon.addCommand(helpCommandSpec);
-  converters.addConverter(commandConverterSpec);
-  converters.addConverter(commandsConverterSpec);
-};
-
-exports.shutdown = function() {
-  canon.removeCommand(helpCommandSpec);
-  converters.removeConverter(commandConverterSpec);
-  converters.removeConverter(commandsConverterSpec);
-};
-
-/**
  * Create a block of data suitable to be passed to the help_list.html template
  */
 function getMatchingCommands(prefix) {
   var commands = canon.getCommands().filter(function(command) {
     if (command.hidden) {
       return false;
     }
 
@@ -10042,326 +11124,17 @@ function getSubCommands(command) {
 
   subcommands.sort(function(c1, c2) {
     return c1.name.localeCompare(c2.name);
   });
 
   return subcommands;
 }
 
-});
-/*
- * Copyright 2012, Mozilla Foundation and contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-define('gcli/converters', ['require', 'exports', 'module' , 'util/util', 'util/promise'], function(require, exports, module) {
-
-'use strict';
-
-var util = require('util/util');
-var promise = require('util/promise');
-
-// It's probably easiest to read this bottom to top
-
-/**
- * Best guess at creating a DOM element from random data
- */
-var fallbackDomConverter = {
-  from: '*',
-  to: 'dom',
-  exec: function(data, conversionContext) {
-    if (data == null) {
-      return conversionContext.document.createTextNode('');
-    }
-
-    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
-      return data;
-    }
-
-    var node = util.createElement(conversionContext.document, 'p');
-    util.setContents(node, data.toString());
-    return node;
-  }
-};
-
-/**
- * Best guess at creating a string from random data
- */
-var fallbackStringConverter = {
-  from: '*',
-  to: 'string',
-  exec: function(data, conversionContext) {
-    if (data.isView) {
-      return data.toDom(conversionContext.document).textContent;
-    }
-
-    if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
-      return data.textContent;
-    }
-
-    return data == null ? '' : data.toString();
-  }
-};
-
-/**
- * Convert a view object to a DOM element
- */
-var viewDomConverter = {
-  from: 'view',
-  to: 'dom',
-  exec: function(view, conversionContext) {
-    return view.toDom(conversionContext.document);
-  }
-};
-
-/**
- * Convert a view object to a string
- */
-var viewStringConverter = {
-  from: 'view',
-  to: 'string',
-  exec: function(view, conversionContext) {
-    return view.toDom(conversionContext.document).textContent;
-  }
-};
-
-/**
- * Convert a terminal object (to help traditional CLI integration) to an element
- */
-var terminalDomConverter = {
-  from: 'terminal',
-  to: 'dom',
-  createTextArea: function(text, conversionContext) {
-    var node = util.createElement(conversionContext.document, 'textarea');
-    node.classList.add('gcli-row-subterminal');
-    node.readOnly = true;
-    node.textContent = text;
-    return node;
-  },
-  exec: function(data, context) {
-    if (Array.isArray(data)) {
-      var node = util.createElement(conversionContext.document, 'div');
-      data.forEach(function(member) {
-        node.appendChild(this.createTextArea(member, conversionContext));
-      });
-      return node;
-    }
-    return this.createTextArea(data);
-  }
-};
-
-/**
- * Convert a terminal object to a string
- */
-var terminalStringConverter = {
-  from: 'terminal',
-  to: 'string',
-  exec: function(data, context) {
-    return Array.isArray(data) ? data.join('') : '' + data;
-  }
-};
-
-/**
- * Several converters are just data.toString inside a 'p' element
- */
-function nodeFromDataToString(data, conversionContext) {
-  var node = util.createElement(conversionContext.document, 'p');
-  node.textContent = data.toString();
-  return node;
-}
-
-/**
- * Convert a string to a DOM element
- */
-var stringDomConverter = {
-  from: 'string',
-  to: 'dom',
-  exec: nodeFromDataToString
-};
-
-/**
- * Convert a number to a DOM element
- */
-var numberDomConverter = {
-  from: 'number',
-  to: 'dom',
-  exec: nodeFromDataToString
-};
-
-/**
- * Convert a number to a DOM element
- */
-var booleanDomConverter = {
-  from: 'boolean',
-  to: 'dom',
-  exec: nodeFromDataToString
-};
-
-/**
- * Convert a number to a DOM element
- */
-var undefinedDomConverter = {
-  from: 'undefined',
-  to: 'dom',
-  exec: function(data, conversionContext) {
-    return util.createElement(conversionContext.document, 'span');
-  }
-};
-
-/**
- * Convert a string to a DOM element
- */
-var errorDomConverter = {
-  from: 'error',
-  to: 'dom',
-  exec: function(ex, conversionContext) {
-    var node = util.createElement(conversionContext.document, 'p');
-    node.className = "gcli-error";
-    node.textContent = ex;
-    return node;
-  }
-};
-
-/**
- * Create a new converter by using 2 converters, one after the other
- */
-function getChainConverter(first, second) {
-  if (first.to !== second.from) {
-    throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from);
-  }
-  return {
-    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));
-};
-
-exports.addConverter(viewDomConverter);
-exports.addConverter(viewStringConverter);
-exports.addConverter(terminalDomConverter);
-exports.addConverter(terminalStringConverter);
-exports.addConverter(stringDomConverter);
-exports.addConverter(numberDomConverter);
-exports.addConverter(booleanDomConverter);
-exports.addConverter(undefinedDomConverter);
-exports.addConverter(errorDomConverter);
-
+exports.items = [ help, helpCommand, helpCommands ];
 
 });
 define("text!gcli/commands/help_man.html", [], "\n" +
   "<div>\n" +
   "  <h3>${command.name}</h3>\n" +
   "\n" +
   "  <h4 class=\"gcli-help-header\">\n" +
   "    ${l10n.helpManSynopsis}:\n" +
@@ -10438,70 +11211,71 @@ define("text!gcli/commands/help.css", []
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/commands/pref', ['require', 'exports', 'module' , 'util/l10n', 'gcli/canon', 'gcli/converters', 'gcli/settings', 'text!gcli/commands/pref_set_check.html'], function(require, exports, module) {
+define('gcli/commands/pref', ['require', 'exports', 'module' , 'util/l10n', 'gcli/settings', 'text!gcli/commands/pref_set_check.html'], function(require, exports, module) {
 
 'use strict';
 
 var l10n = require('util/l10n');
-var canon = require('gcli/canon');
-var converters = require('gcli/converters');
 var settings = require('gcli/settings');
 
 /**
  * Record if the user has clicked on 'Got It!'
  */
-var allowSetSettingSpec = {
+var allowSet = {
+  item: 'setting',
   name: 'allowSet',
   type: 'boolean',
   description: l10n.lookup('allowSetDesc'),
   defaultValue: false
 };
-exports.allowSet = undefined;
 
 /**
  * 'pref' command
  */
-var prefCmdSpec = {
+var pref = {
+  item: 'command',
   name: 'pref',
   description: l10n.lookup('prefDesc'),
   manual: l10n.lookup('prefManual')
 };
 
 /**
  * 'pref show' command
  */
-var prefShowCmdSpec = {
+var prefShow = {
+  item: 'command',
   name: 'pref show',
   description: l10n.lookup('prefShowDesc'),
   manual: l10n.lookup('prefShowManual'),
   params: [
     {
       name: 'setting',
       type: 'setting',
       description: l10n.lookup('prefShowSettingDesc'),
       manual: l10n.lookup('prefShowSettingManual')
     }
   ],
-  exec: function Command_prefShow(args, context) {
+  exec: function(args, context) {
     return l10n.lookupFormat('prefShowSettingValue',
                              [ args.setting.name, args.setting.value ]);
   }
 };
 
 /**
  * 'pref set' command
  */
-var prefSetCmdSpec = {
+var prefSet = {
+  item: 'command',
   name: 'pref set',
   description: l10n.lookup('prefSetDesc'),
   manual: l10n.lookup('prefSetManual'),
   params: [
     {
       name: 'setting',
       type: 'setting',
       description: l10n.lookup('prefSetSettingDesc'),
@@ -10510,85 +11284,70 @@ var prefSetCmdSpec = {
     {
       name: 'value',
       type: 'settingValue',
       description: l10n.lookup('prefSetValueDesc'),
       manual: l10n.lookup('prefSetValueManual')
     }
   ],
   exec: function(args, context) {
-    if (!exports.allowSet.value &&
-        args.setting.name !== exports.allowSet.name) {
+    var allowSet = settings.getSetting('allowSet');
+    if (!allowSet.value &&
+        args.setting.name !== allowSet.name) {
       return context.typedData('prefSetWarning', null);
     }
 
     args.setting.value = args.value;
   }
 };
 
-var prefSetWarningConverterSpec = {
+var prefSetWarning = {
+  item: 'converter',
   from: 'prefSetWarning',
   to: 'view',
   exec: function(data, context) {
+    var allowSet = settings.getSetting('settings');
     return context.createView({
       html: require('text!gcli/commands/pref_set_check.html'),
       options: { allowEval: true, stack: 'pref_set_check.html' },
       d