keep keep keep keep keep replace keep keep keep

var main = require('./main.js');
var path = require('path');
var _ = require('underscore');
var fs = require("fs");
var files = require('./files.js');
var deploy = require('./deploy.js');
var buildmessage = require('./buildmessage.js');
var uniload = require('./uniload.js');
var project = require('./project.js').project;
var warehouse = require('./warehouse.js');
var auth = require('./auth.js');
var config = require('./config.js');
var release = require('./release.js');
var Future = require('fibers/future');
var runLog = require('./run-log.js');
var packageClient = require('./package-client.js');
var utils = require('./utils.js');
var httpHelpers = require('./http-helpers.js');
var archinfo = require('./archinfo.js');
var tropohouse = require('./tropohouse.js');
var packageCache = require('./package-cache.js');
var PackageLoader = require('./package-loader.js').PackageLoader;
var PackageSource = require('./package-source.js');
var compiler = require('./compiler.js');
var catalog = require('./catalog.js');
var stats = require('./stats.js');
var unipackage = require('./unipackage.js');

// Given a site name passed on the command line (eg, 'mysite'), return
// a fully-qualified hostname ('mysite.meteor.com').
//
// This is fairly simple for now. It appends 'meteor.com' if the name
// doesn't contain a dot, and it deletes any trailing dots (the
// technically legal hostname 'mysite.com.' is canonicalized to
// 'mysite.com').
//
// In the future, you should be able to make this default to some
// other domain you control, rather than 'meteor.com'.
var qualifySitename = function (site) {
  if (site.indexOf(".") === -1)
    site = site + ".meteor.com";
  while (site.length && site[site.length - 1] === ".")
    site = site.substring(0, site.length - 1);
  return site;
};

// Given a (non necessarily fully qualified) site name from the
// command line, return true if the site is hosted by a Galaxy, else
// false.
var hostedWithGalaxy = function (site) {
  var site = qualifySitename(site);
  return !! require('./deploy-galaxy.js').discoverGalaxy(site);
};

// Get all local packages available. Returns a map from the package name to the
// version record for that package.
var getLocalPackages = function () {
  var ret = {};

  var names = catalog.complete.getAllPackageNames();
  _.each(names, function (name) {
    if (catalog.complete.isLocalPackage(name)) {
      ret[name] = catalog.complete.getLatestVersion(name);
    }
  });

  return ret;
};

var XXX_DEPLOY_ARCH = 'os.linux.x86_64';

///////////////////////////////////////////////////////////////////////////////
// options that act like commands
///////////////////////////////////////////////////////////////////////////////

// Prints the Meteor architecture name of this host
main.registerCommand({
  name: '--arch',
  requiresRelease: false
}, function (options) {
  var archinfo = require('./archinfo.js');
  console.log(archinfo.host());
});

// Prints the current release in use. Note that if there is not
// actually a specific release, we print to stderr and exit non-zero,
// while if there is a release we print to stdout and exit zero
// (making this useful to scripts).
// XXX: What does this mean in our new release-free world?
main.registerCommand({
  name: '--version',
  requiresRelease: false
}, function (options) {
  if (release.current === null) {
    if (! options.appDir)
      throw new Error("missing release, but not in an app?");
    process.stderr.write(
"This project was created with a checkout of Meteor, rather than an\n" +
"official release, and doesn't have a release number associated with\n" +
"it. You can set its release with 'meteor update'.\n");
    return 1;
  }

  if (release.current.isCheckout()) {
    process.stderr.write("Unreleased (running from a checkout)\n");
    return 1;
  }

  console.log("Release " + release.current.name);
});

// Internal use only. For automated testing.
main.registerCommand({
  name: '--long-version',
  requiresRelease: false
}, function (options) {
  if (files.inCheckout()) {
    process.stderr.write("checkout\n");
    return 1;
  } else if (release.current === null) {
    // .meteor/release says "none" but not in a checkout.
    process.stderr.write("none\n");
    return 1;
  } else {
    process.stdout.write(release.current.name + "\n");
    process.stdout.write(files.getToolsVersion() + "\n");
    return 0;
  }
});

// Internal use only. Makes sure that your Meteor install is totally good to go
// (is "airplane safe"). Specifically, make sure that you have built and/or
// downloaded any packages that you need to run your app (ie: ran the constraint
// solver on the .meteor/packages file and then downloaded/built everything in
// the resulting .meteor/versions).
//
// In a checkout, this makes sure that the checkout is "complete" (dev bundle
// downloaded and all NPM modules installed). The use case is, for example,
// cloning an app from github, running this command, then getting on an
// airplane.
//
// XXX: What happens if you run from checkout and want to build all local
// packages, is this a thing right now? Unclear.
main.registerCommand({
  name: '--get-ready',
  requiresApp: true
}, function (options) {

  // We need the package loader to compile our packages, so let's make sure to
  // get one.
  var loader = project.getPackageLoader();

  // Then get the list of packages that we need to get and build.
  var allPackages = project.getVersions();

  var messages = buildmessage.capture(function () {
    _.forEach(allPackages, function (versions, name) {
      // Calling getPackage on the loader will return a unipackage object, which
      // means that the package will be compiled/downloaded. That we throw the
      // package variable away afterwards is immaterial.
      loader.getPackage(name);
    });
  });

  if (messages.hasMessages()) {
    process.stdout.write("\n" + messages.formatMessages());
    return 1;
  };

  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// run
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'run',
  requiresApp: true,
  options: {
    port: { type: String, short: "p", default: '3000' },
    'app-port': { type: String },
    production: { type: Boolean },
    'raw-logs': { type: Boolean },
    settings: { type: String },
    program: { type: String },
    // With --once, meteor does not re-run the project if it crashes
    // and does not monitor for file changes. Intentionally
    // undocumented: intended for automated testing (eg, cli-test.sh),
    // not end-user use. #Once
    once: { type: Boolean }
  }
}, function (options) {
  // XXX factor this out into a {type: host/port}?
  var portMatch = options.port.match(/^(?:(.+):)?([0-9]+)$/);
  if (!portMatch) {
    process.stderr.write(
"run: --port (-p) must be a number or be of the form 'host:port' where\n" +
"port is a number. Try 'meteor help run' for help.\n");
    return 1;
  }
  var proxyHost = portMatch[1] || null;
  var proxyPort = parseInt(portMatch[2]);

  var appHost, appPort;
  if (options['app-port']) {
    var appPortMatch = options['app-port'].match(/^(?:(.+):)?([0-9]+)?$/);
    if (!appPortMatch) {
      process.stderr.write(
"run: --app-port must be a number or be of the form 'host:port' where\n" +
"port is a number. Try 'meteor help run' for help.\n");
      return 1;
    }
    appHost = appPortMatch[1] || null;
    // It's legit to specify `--app-port host:` and still let the port be
    // randomized.
    appPort = appPortMatch[2] ? parseInt(appPortMatch[2]) : null;
  }

  if (release.forced) {
    var appRelease = project.getMeteorReleaseVersion();
    if (release.current.name !== appRelease) {
      console.log("=> Using Meteor %s as requested (overriding Meteor %s)",
                  release.current.name, appRelease);
      console.log();
    }
  }

  auth.tryRevokeOldTokens({timeout: 1000});

  if (options['raw-logs'])
    runLog.setRawLogs(true);

  var runAll = require('./run-all.js');
  return runAll.run(options.appDir, {
    proxyPort: proxyPort,
    proxyHost: proxyHost,
    appPort: appPort,
    appHost: appHost,
    settingsFile: options.settings,
    program: options.program || undefined,
    buildOptions: {
      minify: options.production
    },
    rootUrl: process.env.ROOT_URL,
    mongoUrl: process.env.MONGO_URL,
    oplogUrl: process.env.MONGO_OPLOG_URL,
    once: options.once
  });
});

///////////////////////////////////////////////////////////////////////////////
// create
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'create',
  maxArgs: 1,
  options: {
    list: { type: Boolean },
    example: { type: String },
    package: { type: Boolean }
  }
}, function (options) {

  // Creating a package is much easier than creating an app, so if that's what
  // we are doing, do that first. (For example, we don't springboard to the
  // latest release to create a package if we are inside an app)
  if (options.package) {
    var packageName = options.args[0];

    // Cannot create a package from example yet!
    if (options.example) {
      process.stderr.write("Cannot create a package from example. \n\n");
      throw new main.ShowUsage;
    }
    // No package examples exist yet.
    if (options.list) {
      process.stderr.write("No package examples exist at this time. \n\n");
      throw new main.ShowUsage;
    }

    if (fs.existsSync(packageName)) {
      process.stderr.write(appPath + ": Already exists\n");
      return 1;
    }

    // XXX: Make this cooler.
    files.cp_r(path.join(__dirname, 'skel-pack'), packageName);

    process.stderr.write(packageName + ": created \n");
    return 0;
  }

  // Suppose you have an app A, and from some directory inside that
  // app, you run 'meteor create /my/new/app'. The new app should use
  // the latest available Meteor release, not the release that A
  // uses. So if we were run from inside an app directory, and the
  // user didn't force a release with --release, we need to
  // springboard to the correct release and tools version.
  //
  // (In particular, it's not sufficient to create the new app with
  // this version of the tools, and then stamp on the correct release
  // at the end.)
  if (! release.current.isCheckout() &&
      release.current.name !== release.latestDownloaded() &&
      ! release.forced) {
    throw new main.SpringboardToLatestRelease;
  }

  var exampleDir = path.join(__dirname, '..', 'examples');
  var examples = _.reject(fs.readdirSync(exampleDir), function (e) {
    return (e === 'unfinished' || e === 'other'  || e[0] === '.');
  });

  if (options.list) {
    process.stdout.write("Available examples:\n");
    _.each(examples, function (e) {
      process.stdout.write("  " + e + "\n");
    });
    process.stdout.write("\n" +
"Create a project from an example with 'meteor create --example <name>'.\n");
    return 0;
  };

  var appPath;
  if (options.args.length === 1)
    appPath = options.args[0];
  else if (options.example)
    appPath = options.example;
  else
    throw new main.ShowUsage;

  if (fs.existsSync(appPath)) {
    process.stderr.write(appPath + ": Already exists\n");
    return 1;
  }

  if (files.findAppDir(appPath)) {
    process.stderr.write(
      "You can't create a Meteor project inside another Meteor project.\n");
    return 1;
  }

  var transform = function (x) {
    return x.replace(/~name~/g, path.basename(appPath));
  };

  if (options.example) {
    if (examples.indexOf(options.example) === -1) {
      process.stderr.write(options.example + ": no such example\n\n");
      process.stderr.write("List available applications with 'meteor create --list'.\n");
      return 1;
    } else {
      files.cp_r(path.join(exampleDir, options.example), appPath, {
        ignore: [/^local$/]
      });
    }
  } else {
    files.cp_r(path.join(__dirname, 'skel'), appPath, {
      transformFilename: function (f) {
        return transform(f);
      },
      transformContents: function (contents, f) {
        if ((/(\.html|\.js|\.css)/).test(f))
          return new Buffer(transform(contents.toString()));
        else
          return contents;
      },
      ignore: [/^local$/]
    });
  }

  // We are actually working with a new meteor project at this point, so
  // reorient its path.
  project.setRootDir(appPath);
  project.writeMeteorReleaseVersion(
    release.current.isCheckout() ? "none" : release.current.name);

  process.stderr.write(appPath + ": created");
  if (options.example && options.example !== appPath)
    process.stderr.write(" (from '" + options.example + "' template)");
  process.stderr.write(".\n\n");

  process.stderr.write(
    "To run your new app:\n" +
      "   cd " + appPath + "\n" +
      "   meteor\n");
});

///////////////////////////////////////////////////////////////////////////////
// run-upgrader
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'run-upgrader',
  hidden: true,
  minArgs: 1,
  maxArgs: 1,
  requiresApp: true
}, function (options) {
  var upgrader = options.args[0];

  var upgraders = require("./upgraders.js");
  console.log("%s: running upgrader %s.",
              path.basename(options.appDir), upgrader);
  upgraders.runUpgrader(upgrader);
});

///////////////////////////////////////////////////////////////////////////////
// add
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'add',
  minArgs: 1,
  maxArgs: Infinity,
  requiresApp: true,
  options: {
    // XXX come up with a better option name, like --allow-downgrades
    force: { type: Boolean, required: false }
  }
}, function (options) {

  var failed = false;

  // Refresh the catalog, cacheing the remote package data on the server.
  catalog.official.refresh();

  // Read in existing package dependencies.
  var packages = project.getConstraints();

  // Combine into one object mapping package name to list of
  // constraints, to pass in to the constraint solver.
  var allPackages = project.getCurrentCombinedConstraints();

  // For every package name specified, add it to our list of package
  // constraints. Don't run the constraint solver until you have added all of
  // them -- add should be an atomic operation regardless of the package
  // order. Even though the package file should specify versions of its inputs,
  // we don't specify these constraints until we get them back from the
  // constraint solver.
  var constraints = _.map(options.args, function (packageReq) {
    // XXX maybe upper case is an error instead?
    return utils.splitConstraint(packageReq.toLowerCase());
  });

  _.each(constraints, function (constraint) {
    // Check that the package exists.
    if (! catalog.complete.getPackage(constraint.package)) {
      process.stderr.write(constraint.package + ": no such package\n");
      failed = true;
      return;
    }

    // If the version was specified, check that the version exists.
    if ( constraint.constraint !== null) {
      var versionInfo = catalog.complete.getVersion(
        constraint.package,
        constraint.constraint);
      if (! versionInfo) {
        process.stderr.write(
          constraint.package + "@" + constraint.constraint  + ": no such version\n");
        failed = true;
        return;
      }
    }
    // Check that the constraint is new. If we are already using the package at
    // the same constraint in the app, return from this function.
    if (_.has(packages, constraint.package) &&
        packages[constraint.package] === constraint.constraint) {
      process.stderr.write(constraint.package + " with version constraint " +
                           constraint.constraint + " has already been added.\n");
      failed = true;
    }

    // Add the package to our direct dependency constraints that we get
    // from .meteor/packages.
    packages[constraint.package] = constraint.constraint;

    // Also, add it to all of our combined dependencies.
    allPackages.push(
      _.extend({ packageName: constraint.package },
                 utils.parseVersionConstraint(constraint.constraint)));
  });

  // If the user asked for invalid packages, then the user probably expects a
  // different result than what they are going to get. We have already logged an
  // error, so we should exit.
  if ( failed ) {
    return 1;
  }

  // Get the contents of our versions file. We need to pass them to the
  // constraint solver, because our contract with the user says that we will
  // never downgrade a dependency.
  var versions = project.getVersions();

  // Call the constraint solver.
  var resolverOpts =  {
    previousSolution: versions,
    breaking: !!options.force
  };
  var newVersions = catalog.complete.resolveConstraints(allPackages,
                                               resolverOpts,
                                               { ignoreProjectDeps: true });
  if ( ! newVersions) {
    // XXX: Better error handling.
    process.stderr.write("Cannot resolve package dependencies.");
  }

  // Don't tell the user what all the operations were until we finish -- we
  // don't want to give a false sense of completeness until everything is
  // written to disk.
  var messageLog = [];

  // Remove the versions that don't exist
  var removed = _.difference(_.keys(versions), _.keys(newVersions));
  _.each(removed, function(packageName) {
    messageLog.push("removed dependency on " + packageName);
  });

  // Install the new versions. If all new versions were installed successfully,
  // then change the .meteor/packages and .meteor/versions to match expected
  // reality.
  var downloaded = project.addPackages(constraints, newVersions);

  _.each(newVersions, function(version, packageName) {
    if (failed)
      return;

    if (_.has(versions, packageName) &&
         versions[packageName] === version) {
      // Nothing changed. Skip this.
      return;
    }

    if (! downloaded[packageName] || downloaded[packageName] !== version) {
      // XXX maybe we shouldn't be letting the constraint solver choose
      // things that don't have the right arches?
      process.stderr.write("Package " + packageName +
                           " has no compatible build for version " +
                           version + "\n");
      failed = true;
      return;
    }

    // Add a message to the update logs to show the user what we have done.
    if ( _.contains(options.args, packageName)) {
      // If we asked for this, we will log it later in more detail.
      return;
    }

    // If the previous versions file had this, then we are upgrading, if it did
    // not, then we must be adding this package anew.
    if (_.has(versions, packageName)) {
      messageLog.push("  upgraded " + packageName + " from version " +
                      versions[packageName] +
                      " to version " + newVersions[packageName]);
    } else {
      messageLog.push("  added " + packageName +
                      " at version " + newVersions[packageName]);
    };
  });

  if (failed)
    return 1;

  // Show the user the messageLog of packages we added.
  _.each(messageLog, function (msg) {
    process.stdout.write(msg + "\n");
  });

  // Show the user the messageLog of the packages that they installed.
  process.stdout.write("Successfully added the following packages. \n");
  _.each(constraints, function (constraint) {
    var version = newVersions[constraint.package];
    var versionRecord = catalog.complete.getVersion(constraint.package, version);
    if (constraint.constraint !== null &&
        version !== constraint.constraint) {
      process.stdout.write("Added " + constraint.package + " at version " + version +
                           " to avoid conflicting dependencies. \n");
    }
    process.stdout.write(constraint.package + " : " + versionRecord.description + "\n");
  });

  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// remove
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'remove',
  minArgs: 1,
  maxArgs: Infinity,
  requiresApp: true
}, function (options) {
  // Refresh the catalog, checking the remote package data on the
  // server. Technically, we don't need to do this, since it is unlikely that
  // new data will change our constraint solver decisions. But as a user, I
  // would expect this command to update the local catalog.
  catalog.official.refresh(true);

  // Read in existing package dependencies.
  var packages = project.getConstraints();

  // For each package name specified, check if we already have it and warn the
  // user. Because removing each package is a completely atomic operation that
  // has no chance of failure, this is just a warning message, it doesn't cause
  // us to stop.
  _.each(options.args, function (packageName) {
    // Check that we are using the package. We don't check if the package
    // exists. You should be able to remove non-existent packages.
    if (! _.has(packages, packageName)) {
      process.stderr.write( packageName  + " is not in this project \n");
    }
  });

  // Get the contents of our versions file, we will want them in order to remove
  // to the user what we removed.
  var versions = project.getVersions();

  // Remove the packages from the project! There is really no way for this to
  // fail, unless something has gone horribly wrong, so we don't need to check
  // for it.
  project.removePackages(options.args);

  // Retrieve the new dependency versions that we have chosen for this project
  // and do some pretty output reporting.
  var newVersions = project.getVersions();

  // Remove the versions that don't exist
  var removed = _.difference(_.keys(versions), _.keys(newVersions));
  _.each(removed, function(packageName) {
    process.stdout.write("  removed dependency on " + packageName + "\n");
  });

  // Log that we removed the constraints. It is possible that there are
  // constraints that we officially removed that the project still 'depends' on,
  // which is why there are these two tiers of error messages.
  _.each(options.args, function (packageName) {
      process.stdout.write("Removed constraint " + packageName + " from project \n");
  });

  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// bundle
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'bundle',
  minArgs: 1,
  maxArgs: 1,
  requiresApp: true,
  options: {
    debug: { type: Boolean },
    directory: { type: Boolean },
    // Undocumented
    'for-deploy': { type: Boolean }
  }
}, function (options) {
  // XXX if they pass a file that doesn't end in .tar.gz or .tgz, add
  // the former for them

  // XXX output, to stderr, the name of the file written to (for human
  // comfort, especially since we might change the name)

  // XXX name the root directory in the bundle based on the basename
  // of the file, not a constant 'bundle' (a bit obnoxious for
  // machines, but worth it for humans)

  var buildDir = path.join(options.appDir, '.meteor', 'local', 'build_tar');
  var outputPath = path.resolve(options.args[0]); // get absolute path
  var bundlePath = options['directory'] ?
      outputPath : path.join(buildDir, 'bundle');

  var bundler = require(path.join(__dirname, 'bundler.js'));
  var loader = project.getPackageLoader();
  stats.recordPackages(options.appDir);

  var bundleResult = bundler.bundle({
    outputPath: bundlePath,
    nodeModulesMode: options['for-deploy'] ? 'skip' : 'copy',
    buildOptions: {
      minify: ! options.debug,
      arch: XXX_DEPLOY_ARCH  // XXX should do this in deploy instead but it's easier to test with bundle
    }
  });
  if (bundleResult.errors) {
    process.stdout.write("Errors prevented bundling:\n");
    process.stdout.write(bundleResult.errors.formatMessages());
    return 1;
  }

  if (!options['directory']) {
    try {
      files.createTarball(path.join(buildDir, 'bundle'), outputPath);
    } catch (err) {
      console.log(JSON.stringify(err));
      process.stderr.write("Couldn't create tarball\n");
    }
  }
  files.rm_recursive(buildDir);
});

///////////////////////////////////////////////////////////////////////////////
// mongo
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'mongo',
  maxArgs: 1,
  options: {
    url: { type: Boolean, short: 'U' }
  },
  requiresApp: function (options) {
    return options.args.length === 0;
  }
}, function (options) {
  var mongoUrl;
  var usedMeteorAccount = false;

  if (options.args.length === 0) {
    // localhost mode
    var findMongoPort =
      require('./run-mongo.js').findMongoPort;
    var mongoPort = findMongoPort(options.appDir);

    // XXX detect the case where Meteor is running, but MONGO_URL was
    // specified?

    if (! mongoPort) {
      process.stdout.write(
"mongo: Meteor isn't running a local MongoDB server.\n" +
"\n" +
"This command only works while Meteor is running your application\n" +
"locally. Start your application first. (This error will also occur if\n" +
"you asked Meteor to use a different MongoDB server with $MONGO_URL when\n" +
"you ran your application.)\n" +
"\n" +
"If you're trying to connect to the database of an app you deployed\n" +
"with 'meteor deploy', specify your site's name with this command.\n"
);
      return 1;
    }
    mongoUrl = "mongodb://127.0.0.1:" + mongoPort + "/meteor";

  } else {
    // remote mode
    var site = qualifySitename(options.args[0]);
    config.printUniverseBanner();

    if (hostedWithGalaxy(site)) {
      var deployGalaxy = require('./deploy-galaxy.js');
      mongoUrl = deployGalaxy.temporaryMongoUrl(site);
    } else {
      mongoUrl = deploy.temporaryMongoUrl(site);
      usedMeteorAccount = true;
    }

    if (! mongoUrl)
      // temporaryMongoUrl() will have printed an error message
      return 1;
  }
  if (options.url) {
    console.log(mongoUrl);
  } else {
    if (usedMeteorAccount)
      auth.maybePrintRegistrationLink();
    process.stdin.pause();
    var runMongo = require('./run-mongo.js');
    runMongo.runMongoShell(mongoUrl);
    throw new main.WaitForExit;
  }
});

///////////////////////////////////////////////////////////////////////////////
// reset
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'reset',
  // Doesn't actually take an argument, but we want to print an custom
  // error message if they try to pass one.
  maxArgs: 1,
  requiresApp: true
}, function (options) {
  if (options.args.length !== 0) {
    process.stderr.write(
"meteor reset only affects the locally stored database.\n" +
"\n" +
"To reset a deployed application use\n" +
"  meteor deploy --delete appname\n" +
"followed by\n" +
"  meteor deploy appname\n");
    return 1;
  }

  // XXX detect the case where Meteor is running the app, but
  // MONGO_URL was set, so we don't see a Mongo process

  var findMongoPort =
    require(path.join(__dirname, 'run-mongo.js')).findMongoPort;
  var isRunning = !! findMongoPort(options.appDir);
  if (isRunning) {
    process.stderr.write(
"reset: Meteor is running.\n" +
"\n" +
"This command does not work while Meteor is running your application.\n" +
"Exit the running Meteor development server.\n");
    return 1;
  }

  var localDir = path.join(options.appDir, '.meteor', 'local');
  files.rm_recursive(localDir);

  process.stdout.write("Project reset.\n");
});

///////////////////////////////////////////////////////////////////////////////
// deploy
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'deploy',
  minArgs: 1,
  maxArgs: 1,
  options: {
    'delete': { type: Boolean, short: 'D' },
    debug: { type: Boolean },
    settings: { type: String },
    star: { type: String },
    // No longer supported, but we still parse it out so that we can
    // print a custom error message.
    password: { type: String },
    // Shouldn't be documented until the Galaxy release. Marks the
    // application as an admin app, so that it will be available in
    // Galaxy admin interface.
    admin: { type: Boolean }
  },
  requiresApp: function (options) {
    return options.delete || options.star ? false : true;
  }
}, function (options) {
  var site = qualifySitename(options.args[0]);
  config.printUniverseBanner();
  var useGalaxy = hostedWithGalaxy(site);
  var deployGalaxy;

  if (options.delete) {
    if (useGalaxy) {
      deployGalaxy = require('./deploy-galaxy.js');
      return deployGalaxy.deleteApp(site);
    } else {
      return deploy.deleteApp(site);
    }
  }

  if (options.password) {
    if (useGalaxy) {
      process.stderr.write("Galaxy does not support --password.\n");
    } else {
      process.stderr.write(
"Setting passwords on apps is no longer supported. Now there are\n" +
"user accounts and your apps are associated with your account so that\n" +
"only you (and people you designate) can access them. See the\n" +
"'meteor claim' and 'meteor authorized' commands.\n");
    }
    return 1;
  }

  var starball = options.star;
  if (starball && ! useGalaxy) {
    // XXX it would be nice to support this for non-Galaxy deploys too
    process.stderr.write(
"--star: only supported when deploying to Galaxy.\n");
    return 1;
  }

  var loggedIn = auth.isLoggedIn();
  if (! loggedIn) {
    process.stderr.write(
"To instantly deploy your app on a free testing server, just enter your\n" +
"email address!\n" +
"\n");

    if (! auth.registerOrLogIn())
      return 1;
  }

  var buildOptions = {
    minify: ! options.debug
  };

  var deployResult;
  if (useGalaxy) {
    deployGalaxy = require('./deploy-galaxy.js');
    deployResult = deployGalaxy.deploy({
      app: site,
      appDir: options.appDir,
      settingsFile: options.settings,
      starball: starball,
      buildOptions: buildOptions,
      admin: options.admin
    });
  } else {
    deployResult = deploy.bundleAndDeploy({
      appDir: options.appDir,
      site: site,
      settingsFile: options.settings,
      buildOptions: buildOptions
    });
  }

  if (deployResult === 0) {
    auth.maybePrintRegistrationLink({
      leadingNewline: true,
      // If the user was already logged in at the beginning of the
      // deploy, then they've already been prompted to set a password
      // at least once before, so we use a slightly different message.
      firstTime: ! loggedIn
    });
  }

  return deployResult;
});

///////////////////////////////////////////////////////////////////////////////
// logs
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'logs',
  minArgs: 1,
  maxArgs: 1,
  options: {
    // XXX once Galaxy is released, document this
    stream: { type: Boolean, short: 'f' }
  }
}, function (options) {
  var site = qualifySitename(options.args[0]);

  if (hostedWithGalaxy(site)) {
    var deployGalaxy = require('./deploy-galaxy.js');
    var ret = deployGalaxy.logs({
      app: site,
      streaming: options.stream
    });
    if (options.stream && ret === null)
      throw new main.WaitForExit;
    return ret;
  } else {
    return deploy.logs(site);
  }
});

///////////////////////////////////////////////////////////////////////////////
// authorized
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'authorized',
  minArgs: 1,
  maxArgs: 1,
  options: {
    add: { type: String, short: "a" },
    remove: { type: String, short: "r" },
    list: { type: Boolean }
  }
}, function (options) {

  if (options.add && options.remove) {
    process.stderr.write(
      "Sorry, you can only add or remove one user at a time.\n");
    return 1;
  }

  if ((options.add || options.remove) && options.list) {
    process.stderr.write(
"Sorry, you can't change the users at the same time as you're listing them.\n");
    return 1;
  }

  config.printUniverseBanner();
  auth.pollForRegistrationCompletion();
  var site = qualifySitename(options.args[0]);

  if (hostedWithGalaxy(site)) {
    process.stderr.write(
"Sites hosted on Galaxy do not have an authorized user list.\n" +
"Instead, go to your Galaxy dashboard to change the authorized users\n" +
"of your Galaxy.\n");
    return 1;
  }

  if (! auth.isLoggedIn()) {
    process.stderr.write(
      "You must be logged in for that. Try 'meteor login'.\n");
    return 1;
  }

  if (options.add)
    return deploy.changeAuthorized(site, "add", options.add);
  else if (options.remove)
    return deploy.changeAuthorized(site, "remove", options.remove);
  else
    return deploy.listAuthorized(site);
});

///////////////////////////////////////////////////////////////////////////////
// claim
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'claim',
  minArgs: 1,
  maxArgs: 1
}, function (options) {
  config.printUniverseBanner();
  auth.pollForRegistrationCompletion();
  var site = qualifySitename(options.args[0]);

  if (! auth.isLoggedIn()) {
    process.stderr.write(
"You must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, create one by clicking\n" +
"'Sign in' and then 'Create account' at www.meteor.com.\n\n");
    return 1;
  }

  if (hostedWithGalaxy(site)) {
    process.stderr.write(
      "Sorry, you can't claim sites that are hosted on Galaxy.\n");
    return 1;
  }

  return deploy.claim(site);
});

///////////////////////////////////////////////////////////////////////////////
// test-packages
///////////////////////////////////////////////////////////////////////////////

//
// Test your local packages.
//
main.registerCommand({
  name: 'test-packages',
  maxArgs: Infinity,
  options: {
    port: { type: Number, short: "p", default: 3000 },
    deploy: { type: String },
    production: { type: Boolean },
    settings: { type: String },
    // Undocumented. See #Once
    once: { type: Boolean },
    // Undocumented. To ensure that QA covers both
    // PollingObserveDriver and OplogObserveDriver, this option
    // disables oplog for tests.  (It still creates a replset, it just
    // doesn't do oplog tailing.)
    'disable-oplog': { type: Boolean },
    // Undocumented flag to use a different test driver.
    'driver-package': { type: String }
  }
}, function (options) {
  var testPackages;
  var localPackageNames = [];
  if (options.args.length === 0) {
    // Only test local packages if no package is specified.
    var packageList = getLocalPackages();
    if (! packageList) {
      // Couldn't load the package list, probably because some package
      // has a parse error. Bail out -- this kind of sucks; we would
      // like to find a way to get reloading.
      return 1;
    }
    testPackages = _.keys(packageList);
  } else {
    var messages = buildmessage.capture(function () {
      testPackages = _.map(options.args, function (p) {
        // If it's a package name, just pass it through.
        if (p.indexOf('/') === -1) {
          if (p.indexOf('@') !== -1) {
            buildmessage.error(
              "You may not specify versions for local packages: " + p );
            // Recover by returning p anyway.
          }
          return p;
        }

        // Otherwise it's a directory; load it into a Package now. Use
        // path.resolve to strip trailing slashes, so that packageName doesn't
        // have a trailing slash.
        //
        // Why use addLocalPackage instead of just loading the packages
        // and passing Unipackage objects to the bundler? Because we
        // actually need the Catalog to know about the package, so that
        // we are able to resolve the test package's dependency on the
        // main package. This is not ideal (I hate how this mutates global
        // state) but it'll do for now.
        var packageDir = path.resolve(p);
        var packageName = path.basename(packageDir);
        catalog.complete.addLocalPackage(packageName, packageDir);
        localPackageNames.push(packageName);
        return packageName;
      });
    });

    if (messages.hasMessages()) {
      process.stdout.write("\n" + messages.formatMessages());
      return 1;
    }
  }

  // Make a temporary app dir (based on the test runner app). This will be
  // cleaned up on process exit. Using a temporary app dir means that we can
  // run multiple "test-packages" commands in parallel without them stomping
  // on each other.
  //
  // Note: testRunnerAppDir deliberately DOES NOT MATCH the app
  // package search path baked into release.current.catalog: we are
  // bundling the test runner app, but finding app packages from the
  // current app (if any).
  var testRunnerAppDir = files.mkdtemp('meteor-test-run');
  files.cp_r(path.join(__dirname, 'test-runner-app'), testRunnerAppDir);

  // We are going to operate in the special test project, so let's remap our
  // main project to the test directory.
  project.setRootDir(testRunnerAppDir);
  project.writeMeteorReleaseVersion(release.current.name || 'none');
  project.forceEditPackages(
    [options['driver-package'] || 'test-in-browser'],
    'add');

  // When we test packages, we need to know their versions and all of their
  // dependencies. We are going to add them to the project and have the project
  // compute them for us. This means that right now, we are testing all packages
  // as they work together.
  var tests = [];
  _.each(testPackages, function(name) {
    var versionRecord = catalog.complete.getLatestVersion(name);
    if (versionRecord && versionRecord.testName) {
      tests.push(versionRecord.testName);
    }
  });

  project.forceEditPackages(tests, 'add');

  var buildOptions = {
    minify: options.production
  };

  var ret;
  if (options.deploy) {
    ret = deploy.bundleAndDeploy({
      appDir: testRunnerAppDir,
      site: options.deploy,
      settingsFile: options.settings,
      buildOptions: buildOptions
    });
  } else {
    var runAll = require('./run-all.js');
    ret = runAll.run(testRunnerAppDir, {
      // if we're testing packages from an app, we still want to make
      // sure the user doesn't 'meteor update' in the app, requiring
      // a switch to a different release
      appDirForVersionCheck: options.appDir,
      proxyPort: options.port,
      disableOplog: options['disable-oplog'],
      settingsFile: options.settings,
      banner: "Tests",
      buildOptions: buildOptions,
      rootUrl: process.env.ROOT_URL,
      mongoUrl: process.env.MONGO_URL,
      oplogUrl: process.env.MONGO_OPLOG_URL,
      once: options.once
    });
  }

  _.each(localPackageNames, function (name) {
    catalog.complete.removeLocalPackage(name);
  });

  return ret;
});

///////////////////////////////////////////////////////////////////////////////
// rebuild
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'rebuild',
  maxArgs: Infinity,
  hidden: true
}, function (options) {
  var messages;
  var count = 0;
  // No packages specified. Rebuild everything.
  if (options.args.length === 0) {
    if (options.appDir) {
      // The catalog doesn't know about other programs in your app. Let's blow
      // away their .build directories if they have them, and not rebuild
      // them. Sort of hacky, but eh.
      var programsDir = project.getProgramsDirectory();
      var programsSubdirs = project.getProgramsSubdirs();
      _.each(programsSubdirs, function (program) {
        // The implementation of this part of the function might change once we
        // change the control file format to explicitly specify packages and
        // programs instead of just loading everything in the programs directory?
        files.rm_recursive(path.join(programsDir, program, '.build.' + program));
      });
    }

    messages = buildmessage.capture(function () {
      count = catalog.complete.rebuildLocalPackages();
    });
  } else {
    messages = buildmessage.capture(function () {
      // Initialize a new package loader, but only for local packages (since we
      // are not going to rebuild non-local packages.)
      var loader = new PackageLoader({
          versions: null,
       });

      _.each(options.args, function (p) {
        // Let's remove the old unipackage directory first.
        var packpath = catalog.complete.getLoadPathForPackage(p, null);
        files.rm_recursive(path.join(packpath, ".build."+p));
        console.log(path.join(packpath, ".build."+p));

        // Getting the package from the package loader will cause it to be
        // rebuilt if it is not built (which it isn't, since we just deleted the
        // unipackage).
        loader.getPackage(p);
        count++;
      });
    });
  }
  if (count)
    console.log("Built " + count + " packages.");
  if (messages.hasMessages()) {
    process.stdout.write("\n" + messages.formatMessages());
    return 1;
  }
});

///////////////////////////////////////////////////////////////////////////////
// login
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'login',
  options: {
    email: { type: String },
    // Undocumented: get credentials on a specific Galaxy. Do we still
    // need this?
    galaxy: { type: String }
  }
}, function (options) {
  return auth.loginCommand(_.extend({
    overwriteExistingToken: true
  }, options));
});

///////////////////////////////////////////////////////////////////////////////
// logout
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'logout'
}, function (options) {
  return auth.logoutCommand(options);
});

///////////////////////////////////////////////////////////////////////////////
// whoami
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'whoami'
}, function (options) {
  return auth.whoAmICommand(options);
});

///////////////////////////////////////////////////////////////////////////////
// admin make-bootstrap-tarballs
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'admin make-bootstrap-tarballs',
  minArgs: 2,
  maxArgs: 2
}, function (options) {
  var releaseNameAndVersion = options.args[0];
  var outputDirectory = options.args[1];

  // In this function, we want to use the official catalog everywhere, because
  // we assume that all packages have been published (along with the release
  // obviously) and we want to be sure to only bundle the published versions.
  catalog.official.refresh();

  var parsed = utils.splitConstraint(releaseNameAndVersion);
  if (!parsed.constraint)
    throw new main.ShowUsage;

  var release = catalog.official.getReleaseVersion(parsed.package,
                                                parsed.constraint);
  if (!release) {
    // XXX this could also mean package unknown.
    process.stderr.write('Release unknown: ' + releaseNameAndVersion + '\n');
    return 1;
  }

  var toolPkg = release.tool && utils.splitConstraint(release.tool);
  if (! (toolPkg && toolPkg.constraint))
    throw new Error("bad tool in release: " + toolPkg);
  var toolPkgBuilds = catalog.official.getAllBuilds(
    toolPkg.package, toolPkg.constraint);
  if (!toolPkgBuilds) {
    // XXX this could also mean package unknown.
    process.stderr.write('Tool version unknown: ' + release.tool + '\n');
    return 1;
  }
  if (!toolPkgBuilds.length) {
    process.stderr.write('Tool version has no builds: ' + release.tool + '\n');
    return 1;
  }

  // XXX check to make sure this is the three arches that we want? it's easier
  // during 0.9.0 development to allow it to just decide "ok, i just want to
  // build the OSX tarball" though.
  var buildArches = _.pluck(toolPkgBuilds, 'architecture');
  var osArches = _.map(buildArches, function (buildArch) {
    var subArches = buildArch.split('+');
    var osArches = _.filter(subArches, function (subArch) {
      return subArch.substr(0, 3) === 'os.';
    });
    if (osArches.length !== 1) {
      throw Error("build architecture " + buildArch + "  lacks unique os.*");
    }
    return osArches[0];
  });

  process.stderr.write(
    'Building bootstrap tarballs for architectures ' +
      osArches.join(', ') + '\n');
  // Before downloading anything, check that the catalog contains everything we
  // need for the OSes that the tool is built for.
  _.each(osArches, function (osArch) {
    _.each(release.packages, function (pkgVersion, pkgName) {
      if (!catalog.official.getBuildsForArches(pkgName, pkgVersion, [osArch])) {
        throw Error("missing build of " + pkgName + "@" + pkgVersion +
                    " for " + osArch);
      }
    });
  });

  files.mkdir_p(outputDirectory);

  _.each(osArches, function (osArch) {
    var tmpdir = files.mkdtemp();
    // We're going to build and tar up a tropohouse in a temporary directory; we
    // don't want to use any of our local packages, so we use catalog.official
    // instead of catalog.
    // XXX update to '.meteor' when we combine houses
    var tmpTropo = new tropohouse.Tropohouse(
      path.join(tmpdir, '.meteor0'), catalog.official);
    tmpTropo.maybeDownloadPackageForArchitectures(
      {packageName: toolPkg.package, version: toolPkg.constraint},
      [osArch]);  // XXX 'browser' too?
    _.each(release.packages, function (pkgVersion, pkgName) {
      tmpTropo.maybeDownloadPackageForArchitectures(
        {packageName: pkgName, version: pkgVersion},
        [osArch]);  // XXX 'browser' too?
    });

    // Delete the downloaded-builds directory which basically just has a second
    // copy of everything.  I think it's OK if the first time we try to deploy
    // to Linux from Mac, it has to download a bunch of stuff.  (Alternatively,
    // we could actually always include Linux64 in the bootstrap tarball, but
    // meh.)
    // XXX it's not like cross-linking even works yet anyway
    files.rm_recursive(path.join(tmpTropo.root, 'downloaded-builds'));

    // XXX should we include some sort of preliminary package-metadata as well?
    // maybe with release info about the release we are using?

    // Create the top-level 'meteor' symlink, which links to the latest tool's
    // meteor shell script.
    var toolUnipackagePath =
          tmpTropo.packagePath(toolPkg.package, toolPkg.constraint);
    var toolUnipackage = new unipackage.Unipackage;
    toolUnipackage.initFromPath(toolPkg.package, toolUnipackagePath);
    var toolRecord = _.findWhere(toolUnipackage.toolsOnDisk, {arch: osArch});
    if (!toolRecord)
      throw Error("missing tool for " + osArch);
    fs.symlinkSync(
      path.join(
        tmpTropo.packagePath(toolPkg.package, toolPkg.constraint, true),
        toolRecord.path,
        'meteor'),
      path.join(tmpTropo.root, 'meteor'));

    files.createTarball(
      tmpTropo.root,
      path.join(outputDirectory, 'meteor-bootstrap-' + osArch + '.tar.gz'));
  });

  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// admin set-banners
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'admin set-banners',
  minArgs: 1,
  maxArgs: 1
}, function (options) {
  var bannersFile = options.args[0];
  try {
    var bannersData = fs.readFileSync(bannersFile, 'utf8');
    bannersData = JSON.parse(bannersData);
  } catch (e) {
    process.stderr.write("Could not parse banners file: ");
    process.stderr.write(e.message + "\n");
    return 1;
  }
  if (!bannersData.track) {
    process.stderr.write("Banners file should have a 'track' key.\n");
    return 1;
  }
  if (!bannersData.banners) {
    process.stderr.write("Banners file should have a 'banners' key.\n");
    return 1;
  }

  try {
    var conn = packageClient.loggedInPackagesConnection();
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }

  conn.call('setBannersOnReleases', bannersData.track,
            bannersData.banners);

  // Refresh afterwards.
  catalog.official.refresh();
  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// self-test
///////////////////////////////////////////////////////////////////////////////

// XXX we should find a way to make self-test fully self-contained, so that it
// ignores "packageDirs" (ie, it shouldn't fail just because you happen to be
// sitting in an app with packages that don't build)

main.registerCommand({
  name: 'self-test',
  minArgs: 0,
  maxArgs: 1,
  options: {
    changed: { type: Boolean },
    'force-online': { type: Boolean },
    slow: { type: Boolean },
    history: { type: Number }
  },
  hidden: true
}, function (options) {
  var selftest = require('./selftest.js');

  // Auto-detect whether to skip 'net' tests, unless --force-online is passed.
  var offline = false;
  if (!options['force-online']) {
    try {
      require('./http-helpers.js').getUrl("http://www.google.com/");
    } catch (e) {
      if (e instanceof files.OfflineError)
        offline = true;
    }
  }

  var testRegexp = undefined;
  if (options.args.length) {
    try {
      testRegexp = new RegExp(options.args[0]);
    } catch (e) {
      if (!(e instanceof SyntaxError))
        throw e;
      process.stderr.write("Bad regular expression: " + options.args[0] + "\n");
      return 1;
    }
  }

  return selftest.runTests({
    onlyChanged: options.changed,
    offline: offline,
    includeSlowTests: options.slow,
    historyLines: options.history,
    testRegexp: testRegexp
  });
});

///////////////////////////////////////////////////////////////////////////////
// list-sites
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'list-sites',
  minArgs: 0,
  maxArgs: 0
}, function (options) {
  auth.pollForRegistrationCompletion();
  if (! auth.isLoggedIn()) {
    process.stderr.write(
      "You must be logged in for that. Try 'meteor login'.\n");
    return 1;
  }

  return deploy.listSites();
});

///////////////////////////////////////////////////////////////////////////////
// dummy
///////////////////////////////////////////////////////////////////////////////

// Dummy test command. Used for automated testing of the command line
// option parser.

main.registerCommand({
  name: 'dummy',
  options: {
    email: { type: String, short: "e", required: true },
    port: { type: Number, short: "p", default: 3000 },
    url: { type: Boolean, short: "U" },
    'delete': { type: Boolean, short: "D" },
    changed: { type: Boolean }
  },
  maxArgs: 2,
  hidden: true
}, function (options) {
  var p = function (key) {
    if (_.has(options, key))
      return JSON.stringify(options[key]);
    return 'none';
  };

  process.stdout.write(p('email') + " " + p('port') + " " + p('changed') +
                       " " + p('args') + "\n");
  if (options.url)
    process.stdout.write('url\n');
  if (options['delete'])
    process.stdout.write('delete\n');
});


var Scanner = HTMLTools.Scanner;
var getComment = HTMLTools.Parse.getComment;
var getDoctype = HTMLTools.Parse.getDoctype;
var getHTMLToken = HTMLTools.Parse.getHTMLToken;

// "tokenize" is not really a great operation for real use, because
// it ignores the special content rules for tags like "style" and
// "script".
var tokenize = function (input) {
  var scanner = new Scanner(input);
  var tokens = [];
  while (! scanner.isEOF()) {
    var token = getHTMLToken(scanner);
    if (token)
      tokens.push(token);
  }

  return tokens;
};

Tinytest.add("html-tools - comments", function (test) {
  var succeed = function (input, content) {
    var scanner = new Scanner(input);
    var result = getComment(scanner);
    test.isTrue(result);
    test.equal(scanner.pos, content.length + 7);
    test.equal(result, {
      t: 'Comment',
      v: content
    });
  };

  var ignore = function (input) {
    var scanner = new Scanner(input);
    var result = getComment(scanner);;
    test.isFalse(result);
    test.equal(scanner.pos, 0);
  };

  var fatal = function (input, messageContains) {
    var scanner = new Scanner(input);
    var error;
    try {
      getComment(scanner);
    } catch (e) {
      error = e;
    }
    test.isTrue(error);
    if (error)
      test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
  };

  test.equal(getComment(new Scanner("")),
             { t: 'Comment', v: ' hello ' });

  ignore("<!DOCTYPE>");
  ignore("<!-a");
  ignore("<--");
  ignore("<!");
  ignore("abc");
  ignore("<a");

  fatal('', 'cannot contain');
  fatal('', 'must end at first');

  fatal('', 'cannot contain');
  fatal('', 'cannot contain');

  succeed('', '');
  succeed('', '-x');
  succeed('', 'x');
  succeed('', ' hello - - world ');
});

Tinytest.add("html-tools - doctype", function (test) {
  var succeed = function (input, expectedProps) {
    var scanner = new Scanner(input);
    var result = getDoctype(scanner);
    test.isTrue(result);
    test.equal(scanner.pos, result.v.length);
    test.equal(input.slice(0, result.v.length), result.v);
    var actualProps = _.extend({}, result);
    delete actualProps.t;
    delete actualProps.v;
    test.equal(actualProps, expectedProps);
  };

  var fatal = function (input, messageContains) {
    var scanner = new Scanner(input);
    var error;
    try {
      getDoctype(scanner);
    } catch (e) {
      error = e;
    }
    test.isTrue(error);
    if (messageContains)
      test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
  };

  test.equal(getDoctype(new Scanner("<!DOCTYPE html>x")),
             { t: 'Doctype',
               v: '<!DOCTYPE html>',
               name: 'html' });

  test.equal(getDoctype(new Scanner("<!DOCTYPE html SYSTEM 'about:legacy-compat'>x")),
             { t: 'Doctype',
               v: "<!DOCTYPE html SYSTEM 'about:legacy-compat'>",
               name: 'html',
               systemId: 'about:legacy-compat' });

  test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>x")),
             { t: 'Doctype',
               v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN'>",
               name: 'html',
               publicId: '-//W3C//DTD HTML 4.0//EN' });

  test.equal(getDoctype(new Scanner("<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>x")),
             { t: 'Doctype',
               v: "<!DOCTYPE html PUBLIC '-//W3C//DTD HTML 4.0//EN' 'http://www.w3.org/TR/html4/strict.dtd'>",
               name: 'html',
               publicId: '-//W3C//DTD HTML 4.0//EN',
               systemId: 'http://www.w3.org/TR/html4/strict.dtd' });

  succeed('<!DOCTYPE html>', {name: 'html'});
  succeed('<!DOCTYPE htML>', {name: 'html'});
  succeed('<!DOCTYPE HTML>', {name: 'html'});
  succeed('<!doctype html>', {name: 'html'});
  succeed('<!doctYPE html>', {name: 'html'});
  succeed('<!DOCTYPE html \u000c>', {name: 'html'});
  fatal('<!DOCTYPE', 'Expected space');
  fatal('<!DOCTYPE ', 'Malformed DOCTYPE');
  fatal('<!DOCTYPE  ', 'Malformed DOCTYPE');
  fatal('<!DOCTYPE>', 'Expected space');
  fatal('<!DOCTYPE >', 'Malformed DOCTYPE');
  fatal('<!DOCTYPE\u0000', 'Expected space');
  fatal('<!DOCTYPE \u0000', 'Malformed DOCTYPE');
  fatal('<!DOCTYPE html\u0000>', 'Malformed DOCTYPE');
  fatal('<!DOCTYPE html', 'Malformed DOCTYPE');

  succeed('<!DOCTYPE html SYSTEM "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
  succeed('<!doctype HTML system "about:legacy-compat">', {name: 'html', systemId: 'about:legacy-compat'});
  succeed("<!DOCTYPE html SYSTEM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
  succeed("<!dOcTyPe HtMl sYsTeM 'about:legacy-compat'>", {name: 'html', systemId: 'about:legacy-compat'});
  succeed('<!DOCTYPE  html\tSYSTEM\t"about:legacy-compat"   \t>', {name: 'html', systemId: 'about:legacy-compat'});
  fatal('<!DOCTYPE html SYSTE "about:legacy-compat">', 'Expected PUBLIC or SYSTEM');
  fatal('<!DOCTYPE html SYSTE', 'Expected PUBLIC or SYSTEM');
  fatal('<!DOCTYPE html SYSTEM"about:legacy-compat">', 'Expected space');
  fatal('<!DOCTYPE html SYSTEM');
  fatal('<!DOCTYPE html SYSTEM ');
  fatal('<!DOCTYPE html SYSTEM>');
  fatal('<!DOCTYPE html SYSTEM >');
  fatal('<!DOCTYPE html SYSTEM ">">');
  fatal('<!DOCTYPE html SYSTEM "\u0000about:legacy-compat">');
  fatal('<!DOCTYPE html SYSTEM "about:legacy-compat\u0000">');
  fatal('<!DOCTYPE html SYSTEM "');
  fatal('<!DOCTYPE html SYSTEM "">');
  fatal('<!DOCTYPE html SYSTEM \'');
  fatal('<!DOCTYPE html SYSTEM\'a\'>');
  fatal('<!DOCTYPE html SYSTEM about:legacy-compat>');

  succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN">',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN'});
  succeed('<!DOCTYPE html PUBLIC \'-//W3C//DTD HTML 4.0//EN\'>',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN'});
  succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN',
            systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
  succeed('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0//EN" \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN',
            systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
  succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'>',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN',
            systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
  succeed('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\'\t\'http://www.w3.org/TR/REC-html40/strict.dtd\'   >',
          { name: 'html',
            publicId: '-//W3C//DTD HTML 4.0//EN',
            systemId: 'http://www.w3.org/TR/REC-html40/strict.dtd'});
  fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
  fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd\'');
  fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'http://www.w3.org/TR/REC-html40/strict.dtd');
  fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' \'');
  fatal('<!DOCTYPE html public \'-//W3C//DTD HTML 4.0//EN\' ');
  fatal('<!DOCTYPE html public \'- ');
  fatal('<!DOCTYPE html public>');
  fatal('<!DOCTYPE html public "-//W3C//DTD HTML 4.0//EN""http://www.w3.org/TR/REC-html40/strict.dtd">');
});

Tinytest.add("html-tools - tokenize", function (test) {

  var fatal = function (input, messageContains) {
    var error;
    try {
      tokenize(input);
    } catch (e) {
      error = e;
    }
    test.isTrue(error);
    if (messageContains)
      test.isTrue(error.message.indexOf(messageContains) >= 0, error.message);
  };

  test.equal(tokenize(''), []);
  test.equal(tokenize('abc'), [{t: 'Chars', v: 'abc'}]);
  test.equal(tokenize('&'), [{t: 'Chars', v: '&'}]);
  test.equal(tokenize('&'), [{t: 'CharRef', v: '&', cp: [38]}]);
  test.equal(tokenize('ok fine'),
             [{t: 'Chars', v: 'ok'},
              {t: 'CharRef', v: ' ', cp: [32]},
              {t: 'Chars', v: 'fine'}]);

  test.equal(tokenize('ac'),
             [{t: 'Chars',
               v: 'a'},
              {t: 'Comment',
               v: 'b'},
              {t: 'Chars',
               v: 'c'}]);

  test.equal(tokenize(''), [{t: 'Tag', n: 'a'}]);

  fatal('<');
  fatal('<x');
  fatal('<x ');
  fatal('<x a');
  fatal('<x a ');
  fatal('<x a =');
  fatal('<x a = ');
  fatal('<x a = b');
  fatal('<x a = "b');
  fatal('<x a = \'b');
  fatal('<x a = b ');
  fatal('<x a = b /');
  test.equal(tokenize('<x a = b />'),
             [{t: 'Tag', n: 'x',
               attrs: { a: [{t: 'Chars', v: 'b'}] },
               isSelfClosing: true}]);

  test.equal(tokenize('X'),
             [{t: 'Tag', n: 'a'},
              {t: 'Chars', v: 'X'},
              {t: 'Tag', n: 'a', isEnd: true}]);

  fatal('<x a a>'); // duplicate attribute value
  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a', attrs: { b: [] }}]);
  fatal('');
  fatal('');
  fatal('');

  // Slash does not end an unquoted attribute, interestingly
  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '/'}] }}]);

  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a',
               attrs: { b: [{t: 'Chars', v: 'c'}],
                        d: [{t: 'Chars', v: 'e'}],
                        f: [{t: 'Chars', v: 'g'}],
                        h: [] }}]);

  fatal('');
  fatal('');
  fatal('');

  test.equal(tokenize(''), [{t: 'Tag', n: 'a', isSelfClosing: true}]);

  fatal('');
  fatal('');
  fatal('');
  fatal('<a b=c<>');

  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a#',
               attrs: { b0: [{t: 'Chars', v: 'c@'}],
                        d1: [{t: 'Chars', v: 'e2'}],
                        'f#': [{t: 'Chars', v: 'g  '}],
                        h: [] }}]);

  test.equal(tokenize('

'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [] }},
              {t: 'Tag', n: 'div', isEnd: true}]);

  test.equal(tokenize('
'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
  test.equal(tokenize('
'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'Chars', v: '&'}] }}]);
  test.equal(tokenize('
'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [{t: 'CharRef', v: '&', cp: [38]}] }}]);

  test.equal(tokenize('
'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [
               {t: 'Chars', v: 'aa&'},
               {t: 'CharRef', v: '𝕫', cp: [120171]},
               {t: 'CharRef', v: '∾̳', cp: [8766, 819]},
               {t: 'Chars', v: '&bb'}
             ] }}]);

  test.equal(tokenize('
'),
             [{t: 'Tag', n: 'div', attrs: { 'class': [
               {t: 'Chars', v: 'aa &'},
               {t: 'CharRef', v: '𝕫', cp: [120171]},
               {t: 'CharRef', v: '∾̳', cp: [8766, 819]},
               {t: 'Chars', v: '& bb'}
             ] }}]);

  test.equal(tokenize('<a b="\'`<>&">'),
             [{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '\'`<>&'}] }}]);
  test.equal(tokenize('<a b=\'"`<>&\'>'),
             [{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '"`<>&'}] }}]);

  fatal('&gt');
  fatal('&gtc');
  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '&gtc' }] }}]);
  test.equal(tokenize(''),
             [{t: 'Tag', n: 'a', attrs: { b: [{t: 'Chars', v: '&gtc' }] }}]);
  fatal('');
  fatal('');
  fatal('');

  fatal('<!');
  fatal('<!x>');

  fatal('');
  fatal('<{{a}}>');
  fatal(''); // end tag can't have attributes
  fatal(''); // end tag can't be self-closing
  fatal('');
});


var fs = Npm.require('fs');
var path = Npm.require('path');
var less = Npm.require('less');
var Future = Npm.require('fibers/future');

Plugin.registerSourceHandler("less", function (compileStep) {
  // XXX annoying that this is replicated in .css, .less, and .styl
  if (! compileStep.archMatches('browser')) {
    // XXX in the future, might be better to emit some kind of a
    // warning if a stylesheet is included on the server, rather than
    // silently ignoring it. but that would mean you can't stick .css
    // at the top level of your app, which is kind of silly.
    return;
  }

  var source = compileStep.read().toString('utf8');
  var options = {
    filename: compileStep.inputPath,
    // Use fs.readFileSync to process @imports. This is the bundler, so
    // that's not going to cause concurrency issues, and it means that (a)
    // we don't have to use Futures and (b) errors thrown by bugs in less
    // actually get caught.
    syncImport: true,
    paths: [path.dirname(compileStep._fullInputPath)] // for @import
  };

  var parser = new less.Parser(options);
  var astFuture = new Future;
  var sourceMap = null;
  try {
    parser.parse(source, astFuture.resolver());
    var ast = astFuture.wait();

    var css = ast.toCSS({
      sourceMap: true,
      writeSourceMap: function (sm) {
        sourceMap = JSON.parse(sm);
      }
    });
  } catch (e) {
    // less.Parser.parse is supposed to report any errors via its
    // callback. But sometimes, it throws them instead. This is
    // probably a bug in less. Be prepared for either behavior.
    compileStep.error({
      message: "Less compiler error: " + e.message,
      sourcePath: e.filename || compileStep.inputPath,
      line: e.line,
      column: e.column + 1
    });
    return;
  }

  if (sourceMap) {
    sourceMap.sources = [compileStep.inputPath];
    sourceMap.sourcesContent = [source];
    sourceMap = JSON.stringify(sourceMap);
  }

  compileStep.addStylesheet({
    path: compileStep.inputPath + ".css",
    data: css,
    sourceMap: sourceMap
  });
});;

// Register import.less files with the dependency watcher, without actually
// processing them. There is a similar rule in the stylus package.
Plugin.registerSourceHandler("import.less", function () {
  // Do nothing
});

// Backward compatibility with Meteor 0.7
Plugin.registerSourceHandler("lessimport", function () {});


Package.describe({
  summary: "Require this application to use HTTPS",
  version: "1.0.0"
});

Package.on_use(function (api) {
  api.use('webapp', 'server');
  api.use('underscore');
  // make sure we come after livedata, so we load after the sockjs
  // server has been instantiated.
  api.use('livedata', 'server');

  api.add_files('force_ssl_common.js', ['client', 'server']);
  api.add_files('force_ssl_server.js', 'server');

  // Another thing we could do is add a force_ssl_client.js file that
  // makes sure document.location.protocol is 'https'. If it detected
  // the code was loaded from a non-localhost non-https site, it would
  // stop the app from working and pop up an error box or something.
});


// Delete the downloaded-builds directory which basically just has a second

// copy of everything.  I think it's OK if the first time we try to deploy

keep keep keep replace keep keep keep keep

var path = require("path");
var fs = require("fs");
var os = require("os");
var Future = require("fibers/future");
var _ = require("underscore");
var files = require('./files.js');
var utils = require('./utils.js');
var updater = require('./updater.js');
var httpHelpers = require('./http-helpers.js');
var fiberHelpers = require('./fiber-helpers.js');
var release = require('./release.js');
var archinfo = require('./archinfo.js');
var catalog = require('./catalog.js');
var Unipackage = require('./unipackage.js').Unipackage;

exports.Tropohouse = function (root, catalog) {
  var self = this;
  self.root = root;
  self.catalog = catalog;
};

// Return the directory containing our loaded collection of tools, releases and
// packages. If we're running an installed version, found at $HOME/.meteor, if
// we are running form a checkout, probably at $CHECKOUT_DIR/.meteor.
var defaultWarehouseDir = function () {
  // a hook for tests, or i guess for users.
  if (process.env.METEOR_WAREHOUSE_DIR)
    return process.env.METEOR_WAREHOUSE_DIR;

  var warehouseBase = files.inCheckout()
     ? files.getCurrentToolsDir() : process.env.HOME;
  // XXX This will be `.meteor` soon, once we've written the code to make the
  // tropohouse and warehouse live together in harmony (eg, allowing tropohouse
  // tools to springboard to warehouse tools).
  return path.join(warehouseBase, ".meteor0");
};

// The default tropohouse is on disk at defaultWarehouseDir() and knows not to
// download local packages; you can make your own Tropohouse to override these
// things.
exports.default = new exports.Tropohouse(
  defaultWarehouseDir(), catalog.complete);

_.extend(exports.Tropohouse.prototype, {
  // Return the directory within the warehouse that would contain downloaded
  // builds for a given package and version, if we have such builds cached on
  // disk. Takes in a package name and a version, and returns the [warehouse
  // path]/downloaded-builds/[packageName]/[version].
  //
  // This does NOT check that the directory exists or contains content.
  downloadedBuildsDirectory: function(packageName, version) {
    var self = this;
    return path.join(self.root, "downloaded-builds", packageName, version);
  },

  // Return a path to a location that would contain a specified build of the
  // package at the specified version, if we have this build cached on disk.
  downloadedBuildPath: function(packageName, version, buildArches) {
    var self = this;
    return path.join(self.downloadedBuildsDirectory(packageName, version),
                     buildArches);
  },

  // Returns a list of builds that we have downloaded for a package&version by
  // reading the contents of that package & version's build directory. Does not
  // check that the directory exists.
  downloadedBuilds: function (packageName, version) {
    var self = this;
    return files.readdirNoDots(
      self.downloadedBuildsDirectory(packageName, version));
  },

  // Returns a list of architectures that we have downloaded for a given package
  // at a version: gets a list of builds from downloadedBuilds and then splits
  // up each build into its component architectures, and returns a union of all
  // contained architectures.
  downloadedArches: function (packageName, version) {
    var self = this;
    var downloadedBuilds = self.downloadedBuilds(packageName, version);
    var downloadedArches = _.reduce(
      downloadedBuilds,
      function(init, build) {
        return _.union(build.split('+'), init);
      },
      []);
    return downloadedArches;
  },

  // Returns the load path where one can expect to find the package, at a given
  // version, if we have already downloaded from the package server. Does not
  // check for contents.
  //
  // Returns null if the package name is lexographically invalid.
  packagePath: function (packageName, version, relative) {
    var self = this;
    if (! utils.validPackageName(packageName)) {
      return null;
    }

    var relativePath = path.join("packages", packageName, version);
    return relative ? relativePath : path.join(self.root, relativePath);
  },

  // Contacts the package server, downloads and extracts a tarball for a given
  // buildRecord into the warehouse downloadedBuildPath for that build.
  //
  // XXX: Error handling.
  downloadSpecifiedBuild: function (buildRecord) {
    var self = this;
    // XXX nb: "version" field is calculated by getBuildsForArches
    var path = self.downloadedBuildPath(
      buildRecord.packageName, buildRecord.version, buildRecord.architecture);
    var packageTarball = httpHelpers.getUrl({
      url: buildRecord.build.url,
      encoding: null
    });
    files.extractTarGz(packageTarball, path);
  },

  // Given versionInfo for a package version and required architectures, checks
  // to make sure that we have the package at the requested arch. If we do not
  // have the package, contact the server and attempt to download and extract
  // the right build. Returns true once we have the package, or false if the
  // correct build does not exist on the package server.
  //
  // XXX more precise error handling in offline case. maybe throw instead like
  // warehouse does.
  //
  // XXX this is kinda bogus right now and needs to be fixed when we actually
  // get around to implement cross-linking (which is the point)
  maybeDownloadPackageForArchitectures: function (versionInfo, requiredArches) {
    var self = this;
    var packageName = versionInfo.packageName;
    var version = versionInfo.version;

    // If this package isn't coming from the package server (loaded from a
    // checkout, or from an app package directory), don't try to download it (we
    // already have it)
    if (self.catalog.isLocalPackage(packageName))
      return true;

    var packageDir = self.packagePath(packageName, version);
    if (fs.existsSync(packageDir)) {
      // Package exists for this build, so we are good.

      // XXX this doesn't actually work! the point of this whole thing is that
      // you can fat-ify a package (eg, at deploy time) but this here assumes
      // that once you write a package you'll never write it again.
      return true;
    }

    // Figure out what arches (if any) we have downloaded for this package
    // version already.
    var downloadedArches = self.downloadedArches(packageName, version);
    var archesToDownload = _.filter(requiredArches, function (requiredArch) {
      return !archinfo.mostSpecificMatch(requiredArch, downloadedArches);
    });

    if (archesToDownload.length) {
      var buildsToDownload = self.catalog.getBuildsForArches(
        packageName, version, archesToDownload);
      if (! buildsToDownload) {
        // XXX throw a special error instead?
        return false;
      }

      // XXX how does concurrency work here?  we could just get errors if we try
      // to rename over the other thing?  but that's the same as in warehouse?
      _.each(buildsToDownload, function (build) {
        self.downloadSpecifiedBuild(build);
      });
    }

    // We need to turn our builds into a unipackage.
    var unipackage = new Unipackage;
    var builds = self.downloadedBuilds(packageName, version);
    _.each(builds, function (build, i) {
      unipackage._loadBuildsFromPath(
        packageName,
        self.downloadedBuildPath(packageName, version, build),
        {firstUnipackage: i === 0});
    });
    // XXX save new buildinfo stuff so it auto-rebuilds
    unipackage.saveToPath(packageDir);

    return true;
  },

  latestMeteorSymlink: function () {
    var self = this;
    var linkPath = path.join(self.root, 'meteor');
    return fs.readlinkSync(linkPath);
  },

  replaceLatestMeteorSymlink: function (linkText) {
    var self = this;
    var linkPath = path.join(self.root, 'meteor');
    files.symlinkOverSync(linkText, linkPath);
  }
});


Each client creates a room when it starts up and joins that room plus
`roomsPerClient` other rooms.

Every `talkativePeriodSeconds`, each client has a
`chanceClientIsTalkative` probability of being "talkative". If it is
talkative for that period, it will send one `messageSize` sized message
every `talkativeMessagesPerSecond` for the duration of the period.

Old messages are deleted every `messageHistorySeconds` seconds. Old
rooms are deleted every `roomHistorySeconds`. `roomHistorySeconds`
should line up with the length a particular client dwells on the site
(typically part of the phantomjs script used to drive the clients.)


// White box tests of invalidation crossbar matching function.
// Note: the current crossbar match function is designed specifically
// to ensure that a modification that targets a specific ID does not
// notify a query that is watching a different specific ID. (And to
// keep separate collections separate.) Other than that, there's no
// deep meaning to the matching function, and it could be changed later
// as long as it preserves that property.
Tinytest.add('livedata - crossbar', function (test) {
  var crossbar = new DDPServer._Crossbar;
  test.isTrue(crossbar._matches({collection: "C"},
                                {collection: "C"}));
  test.isTrue(crossbar._matches({collection: "C", id: "X"},
                                {collection: "C"}));
  test.isTrue(crossbar._matches({collection: "C"},
                                {collection: "C", id: "X"}));
  test.isTrue(crossbar._matches({collection: "C", id: "X"},
                                {collection: "C"}));

  test.isFalse(crossbar._matches({collection: "C", id: "X"},
                                 {collection: "C", id: "Y"}));

  // Test that stopped listens definitely don't fire.
  var calledFirst = false;
  var calledSecond = false;
  var trigger = {collection: "C"};
  var secondHandle;
  crossbar.listen(trigger, function (notification) {
    // This test assumes that listeners will be called in the order
    // registered. It's not wrong for the crossbar to do something different,
    // but the test won't be valid in that case, so make it fail so we notice.
    calledFirst = true;
    if (calledSecond) {
      test.fail({
        type: "test_assumption_failed",
        message: "test assumed that listeners would be called in the order registered"
      });
    } else {
      secondHandle.stop();
    }
  });
  secondHandle = crossbar.listen(trigger, function (notification) {
    // This should not get invoked, because it should be stopped by the other
    // listener!
    calledSecond = true;
  });
  crossbar.fire(trigger);
  test.isTrue(calledFirst);
  test.isFalse(calledSecond);
});


{packageName: toolsPkg, version: toolsVersion},

var packagePath = tropohouse.default.packagePath(toolsPkg, toolsVersion);