var main = require('./main.js');
var _ = require('underscore');
var files = require('./files.js');
var deploy = require('./deploy.js');
var buildmessage = require('./buildmessage.js');
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 PackageSource = require('./package-source.js');
var compiler = require('./compiler.js');
var catalog = require('./catalog.js');
var catalogRemote = require('./catalog-remote.js');
var stats = require('./stats.js');
var isopack = require('./isopack.js');
var updater = require('./updater.js');
var cordova = require('./commands-cordova.js');
var Console = require('./console.js').Console;
var projectContextModule = require('./project-context.js');
var packageMapModule = require('./package-map.js');
var packageVersionParser = require('./package-version-parser.js');
var colonConverter = require("./colon-converter.js");

// For each release (or package), we store a meta-record with its name,
// maintainers, etc. This function takes in a name, figures out if
// it is a release or a package, and fetches the correct record.
//
// Specifically, it returns an object with the following keys:
//  - record : (a package or version record)
//  - isRelease : true if it is a release instead of a package.
var getReleaseOrPackageRecord = function(name) {
  var rec = catalog.official.getPackage(name);
  var rel = false;
  if (!rec) {
    // Not a package! But is it a release track?
    rec = catalog.official.getReleaseTrack(name);
    if (rec)
      rel = true;
  }
  return { record: rec, isRelease: rel };
};

// Seriously, this dies if it can't refresh. Only call it if you're sure you're
// OK that the command doesn't work while offline.
var refreshOfficialCatalogOrDie = function (options) {
  if (!catalog.refreshOrWarn(options)) {
    Console.error(
      "This command requires an up-to-date package catalog. Exiting.");
    throw new main.ExitWithCode(1);
  }
};

// XXX: To formatters.js ?
var formatAsList = function (list, options) {
  options = options || {};
  var formatter = options.formatter || _.identity;
  return _.map(list, formatter).join(", ");
};

var endsWith = function (s, suffix) {
  return s.length >= suffix.length &&
    s.substr(s.length - suffix.length) === suffix;
};

var removeIfEndsWith = function (s, suffix) {
  if (!endsWith(s, suffix)) {
    return s;
  }
  return s.substring(0, s.length - suffix.length);
};

var formatArchitecture = function (s) {
  while (true) {
    var original = s;

    // Remove well-known suffixes
    s = removeIfEndsWith(s, "+web.cordova");
    s = removeIfEndsWith(s, "+web.browser");

    if (original === s) {
      return s;
    }
  }
};

// Internal use only. Makes sure that your Meteor install is totally good to go
// (is "airplane safe"). Specifically, it:
//    - Builds all local packages, even those you're not using in your current
//      app. (If you're not in an app, it still does this even though there is
//      no persistent IsopackCache, because this still causes npm dependencies
//      to be downloaded.)
//    - Ensures that all packages in your current release are downloaded, even
//      those you're not using in your current app.
//    - Ensures that all packages used by your app (if any) are downloaded
// (It also ensures you have the dev bundle downloaded, just like every command
// in a checkout.)
//
// The use case is, for example, cloning an app from github, running this
// command, then getting on an airplane.
//
// This does NOT guarantee a *re*build of all local packages (though it will
// download any new dependencies): we still trust the buildinfo files in your
// app's IsopackCache. If you want to rebuild all local packages that are used
// in your app, call meteor rebuild. That said, rebuild should only be necessary
// if there's a bug in the build tool... otherwise, packages should be rebuilt
// whenever necessary!
main.registerCommand({
  name: '--get-ready',
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false }),
  options: {
    'allow-incompatible-update': { type: Boolean }
  }
}, function (options) {
  // If we're in an app, make sure that we can build the current app. Otherwise
  // just make sure that we can build some fake app.
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir || files.mkdtemp('meteor-get-ready'),
    neverWriteProjectConstraintsFile: true,
    neverWritePackageMap: true,
    allowIncompatibleUpdate: options['allow-incompatible-update']
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.initializeCatalog();
  });

  // Add every local package (including tests) and every release package to this
  // project. (Hopefully they can all be built at once!)
  var addPackages = function (packageNames) {
    projectContext.projectConstraintsFile.addConstraints(
      _.map(packageNames, function (p) {
        return utils.parsePackageConstraint(p);
      })
    );
  };
  addPackages(projectContext.localCatalog.getAllPackageNames());
  if (release.current.isProperRelease()) {
    addPackages(_.keys(release.current.getPackages()));
  }

  // Now finish building and downloading.
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  // We don't display package changes because they'd include all these packages
  // not actually in the app!
  // XXX Maybe we should do a first pass that only builds packages actually in
  // the app and does display the PackageMapDelta?
  return 0;
});


// Internal use only. A simpler version of --get-ready which doesn't try to also
// build/download local and release packages that aren't currently used. Just
// builds and downloads packages used by the current app.
main.registerCommand({
  name: '--prepare-app',
  requiresApp: true,
  catalogRefresh: new catalog.Refresh.Never(),
  options: {
    'allow-incompatible-update': { type: Boolean }
  }
}, function (options) {
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    allowIncompatibleUpdate: options['allow-incompatible-update']
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  projectContext.packageMapDelta.displayOnConsole();
});


///////////////////////////////////////////////////////////////////////////////
// publish a package
///////////////////////////////////////////////////////////////////////////////

// Updates the metadata for a given package version. Prints user-friendly
// messages if certain new values are invalid; calls to the packageClient to
// perform the actual update.
//
// Takes in a packageSource and a connection to the package server. Returns 0 on
// success and an exit code on failure.
var updatePackageMetadata = function (packageSource, conn) {
    var name = packageSource.name;
    var version = packageSource.version;

    // You can't change the metadata of a record that doesn't exist.
    var existingRecord =
          catalog.official.getVersion(name, version);
    if (! existingRecord) {
      Console.error(
        "You can't call",  Console.command("`meteor publish --update`"),
        "on version " + version + " of " + "package '" + name +
          "' without publishing it first.");
      return 1;
    }

    // Load in the user's documentation, and check that it isn't blank.
    var readmeInfo;
    main.captureAndExit(
      "=> Errors while publishing:", "reading documentation",
      function () {
       readmeInfo = packageSource.processReadme();
    });

    // You are still not allowed to upload a blank README.md.
    if (readmeInfo && readmeInfo.hash === files.blankHash) {
      Console.error(
        "Your documentation file is blank, so users may have trouble",
        "figuring out how to use your package. Please fill it out, or",
        "set 'documentation: null' in your Package.describe.");
      return 1;
    };

    // Finally, call to the server.
    main.captureAndExit(
      "=> Errors while publishing:",
      "updating package metadata",
      function () {
        packageClient.updatePackageMetadata({
          packageSource: packageSource,
          readmeInfo: readmeInfo,
          connection: conn
        });
    });

    Console.info(
      "Success. You can take a look at the new metadata by running",
      Console.command("'meteor show " + name + "@" + version + "'"),
      "outside the current project directory.");

    // Refresh, so that we actually learn about the thing we just published.
    refreshOfficialCatalogOrDie();
    return 0;
}

main.registerCommand({
  name: 'publish',
  minArgs: 0,
  maxArgs: 0,
  options: {
    create: { type: Boolean },
    update: { type: Boolean },
    // This is similar to publish-for-arch, but uses the source code you have
    // locally (and other local packages you may have) instead of downloading
    // the source bundle. It does verify that the source is the same, though.
    // Good for bootstrapping things in the core release.
    'existing-version': { type: Boolean },
    // This is the equivalent of "sudo": make sure that administrators don't
    // accidentally put their personal packages in the top level namespace.
    'top-level': { type: Boolean }
  },
  requiresPackage: true,
  // We optimize the workflow by using up-to-date package data to weed out
  // obviously incorrect submissions before they ever hit the wire.
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false }),
  'allow-incompatible-update': { type: Boolean }
}, function (options) {
  if (options.create && options['existing-version']) {
    // Make up your mind!
    Console.error(
      "The --create and --existing-version options cannot",
      "both be specified.");
    return 1;
  }

  if (options.update && options.create) {
    Console.error(
      "The --create and --update options cannot both be specified.");
    return 1;
  }

  if (options.update && options["existing-version"]) {
    Console.error(
      "The --update option implies that the version already exists.",
      "You do not need to use the --existing-version flag with --update.");
    return 1;
  }

  var projectContext;
  if (! options.appDir) {
    // We're not in an app? OK, make a temporary app directory, and make sure
    // that the current package directory is found by its local catalog.
    var tempProjectDir = files.mkdtemp('meteor-package-build');
    projectContext = new projectContextModule.ProjectContext({
      projectDir: tempProjectDir,  // won't have a packages dir, that's OK
      explicitlyAddedLocalPackageDirs: [options.packageDir],
      packageMapFilename: files.pathJoin(options.packageDir, '.versions'),
      // We always want to write our '.versions' package map, overriding a
      // comparison against the value of a release file that doesn't exist.
      alwaysWritePackageMap: true,
      // When we publish, we should always include web.cordova unibuilds, even
      // though this temporary directory does not have any cordova platforms
      forceIncludeCordovaUnibuild: true,
      allowIncompatibleUpdate: options['allow-incompatible-update']
    });
  } else {
    // We're in an app; let the app be our context, but make sure we don't
    // overwrite .meteor/packages or .meteor/versions when we add some temporary
    // constraints (which ensure that we can actually build the package and its
    // tests).
    projectContext = new projectContextModule.ProjectContext({
      projectDir: options.appDir,
      neverWriteProjectConstraintsFile: true,
      neverWritePackageMap: true,
      // When we publish, we should always include web.cordova unibuilds, even
      // if this project does not have any cordova platforms
      forceIncludeCordovaUnibuild: true,
      allowIncompatibleUpdate: options['allow-incompatible-update']
    });
  }

  main.captureAndExit("=> Errors while initializing project:", function () {
    // Just get up to initializing the catalog. We're going to mutate the
    // constraints file a bit before we prepare the build.
    projectContext.initializeCatalog();
  });

  // Connect to the package server and log in.
  try {
    var conn = packageClient.loggedInPackagesConnection();
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }
  if (! conn) {
    Console.error('No connection: Publish failed.');
    return 1;
  }

  var localVersionRecord = projectContext.localCatalog.getVersionBySourceRoot(
    options.packageDir);
  if (! localVersionRecord) {
    // OK, we're inside a package (ie, a directory with a package.js) and we're
    // inside an app (ie, a directory with a file named .meteor/packages) but
    // the package is not on the app's search path (ie, it's probably not
    // directly inside the app's packages directory).  That's kind of
    // weird. Let's not allow this.
    Console.error(
      "The package you are in appears to be inside a Meteor app but is not " +
       "in its packages directory. You may only publish packages that are " +
       "entirely outside of a project or that are loaded by the project " +
       "that they are inside.");
    return 1;
  }
  var packageName = localVersionRecord.packageName;
  var packageSource = projectContext.localCatalog.getPackageSource(packageName);
  if (! packageSource)
    throw Error("no PackageSource for " + packageName);

  // Anything published to the server must explicitly set a version.
  if (! packageSource.versionExplicitlyProvided) {
    Console.error("A version must be specified for the package. Set it with " +
                  "Package.describe.");
    return 1;
  }

  // If we just want to update the package metadata, then we have all we
  // need. Don't bother building the package, just update the metadata and
  // return the result.
  if (options.update) {
    return updatePackageMetadata(packageSource, conn);
  }

  // Fail early if the package record exists, but we don't think that it does
  // and are passing in the --create flag!
  if (options.create) {
    var packageInfo = catalog.official.getPackage(packageName);
    if (packageInfo) {
      Console.error(
        "Package already exists. To create a new version of an existing "+
        "package, do not use the --create flag!");
      return 2;
    }

    if (!options['top-level'] && !packageName.match(/:/)) {
      Console.error(
        "Only administrators can create top-level packages without an",
        "account prefix. (To confirm that you wish to create a top-level",
        "package with no account prefix, please run this command again",
        "with the --top-level option.)");
      // You actually shouldn't be able to get here without being logged in, but
      // it seems poor form to assume anything like that for the point of a
      // brief error message.
      if (auth.isLoggedIn()) {
        var properName =  auth.loggedInUsername() + ":" + packageName;
        Console.error(
          "\nDid you mean to create " + properName + " instead?"
       );
      }
      return 2;
    }
  }

  // Make sure that both the package and its test (if any) are actually built.
  _.each([packageName, packageSource.testName], function (name) {
    if (! name)  // for testName
      return;
    // If we're already using this package, that's OK; no need to override.
    if (projectContext.projectConstraintsFile.getConstraint(name))
      return;
    projectContext.projectConstraintsFile.addConstraints(
      [utils.parsePackageConstraint(name)]);
  });

  // Now resolve constraints and build packages.
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  // We don't display the package map delta here, because it includes adding the
  // package's test and all the test's dependencies.

  var isopack = projectContext.isopackCache.getIsopack(packageName);
  if (! isopack) {
    // This shouldn't happen; we already threw a different error if the package
    // wasn't even in the local catalog, and we explicitly added this package to
    // the project's constraints file, so it should have been built.
    throw Error("package not built even though added to constraints?");
  }

  // We have initialized everything, so perform the publish operation.
  var binary = isopack.platformSpecific();
  main.captureAndExit(
    "=> Errors while publishing:",
    "publishing the package",
    function () {
      packageClient.publishPackage({
        projectContext: projectContext,
        packageSource: packageSource,
        connection: conn,
        new: options.create,
        existingVersion: options['existing-version'],
        doNotPublishBuild: binary && !options['existing-version']
      });
    });

  Console.info('Published ' + packageName + '@' + localVersionRecord.version +
               '.');

  // We are only publishing one package, so we should close the connection, and
  // then exit with the previous error code.
  conn.close();

  // Warn the user if their package is not good for all architectures.
  if (binary && options['existing-version']) {
    // This is an undocumented command that you are not supposed to run! We
    // assume that you know what you are doing, if you ran it, and are OK with
    // overrwriting normal compatibilities.
    Console.warn();
    Console.labelWarn("Your package contains binary code.");
  } else if (binary) {
    // Normal publish flow. Tell the user nicely.
    Console.warn();
    Console.warn(
      "You're not done publishing yet! This package contains binary code and",
      "must be built on all of Meteor's architectures, including this",
      "machine's architecture.");
    Console.warn();
    Console.info(
      "You can access Meteor provided build machines, pre-configured to",
      "support older versions of MacOS and Linux, by running:");
    _.each(["os.osx.x86_64", "os.linux.x86_64",
            "os.linux.x86_32", "os.windows.x86_32"],
      function (a) {
        Console.info(
          Console.command("meteor admin get-machine " + a),
          Console.options({ indent: 2 }));
    });

    Console.info();
    Console.info("On each machine, run:");
    Console.info();
    Console.info(
      Console.command(
        "meteor publish-for-arch " +
        packageSource.name + "@" + packageSource.version),
      Console.options({ indent: 2 }));
    Console.info();
    Console.info(
      "For more information on binary ABIs and consistent builds, see:");
    Console.info(
      Console.url("https://github.com/meteor/meteor/wiki/Build-Machines"),
      Console.options({ indent: 2 })
    );
    Console.info();
  }

  // Refresh, so that we actually learn about the thing we just published.
  refreshOfficialCatalogOrDie();

  return 0;
});


main.registerCommand({
  name: 'publish-for-arch',
  minArgs: 1,
  maxArgs: 1,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false }),
  // in theory, this option shouldn't be necessary, because when you run
  // publish-for-arch you want to reproduce the exact same setup as when
  // you ran 'publish', but support the option in case it comes up.
  'allow-incompatible-update': { type: Boolean }
}, function (options) {
  // argument processing
  var all = options.args[0].split('@');
  if (all.length !== 2) {
    Console.error(
      'Incorrect argument. Please use the form of <packageName>@<version>');
    throw new main.ShowUsage;
  }
  var name = all[0];
  var versionString = all[1];

  var packageInfo = catalog.official.getPackage(name);
  if (! packageInfo) {
    Console.error(
      "You can't call " + Console.command("`meteor publish-for-arch`") +
      "on package '" + name + "' without " +" publishing it first."
    );
    Console.error();
    Console.error(
      "To publish the package, run " +
       Console.command("`meteor publish --create` ") +
      "from the package directory.");
    Console.error();
    return 1;
  }

  var pkgVersion = catalog.official.getVersion(name, versionString);
  if (! pkgVersion) {
    Console.error(
      "You can't call",  Console.command("`meteor publish-for-arch`"),
      "on version " + versionString + " of " + "package '" + name +
      "' without publishing it first.");
    Console.error();
    Console.error(
      "To publish the package, run " + Console.command("`meteor publish ` ") +
      "from the package directory.");
    Console.error();
    return 1;
  }

  if (! pkgVersion.source || ! pkgVersion.source.url) {
    Console.error(
      "There is no source uploaded for",
      name + '@' + versionString);
    return 1;
  }

  // No releaseName (not even null): this predates the isopack-cache
  // refactorings. Let's just springboard to Meteor 1.0 and let it deal with any
  // further springboarding based on reading a nested json file.
  if (! _.has(pkgVersion, 'releaseName')) {
    if (files.inCheckout()) {
      Console.error(
        "This package was published from an old version of meteor, " +
        "but you are running from checkout! Consider running " +
        Console.command("`meteor --release 1.0`"),
        "so we can springboard correctly.");
      process.exit(1);
    }
    throw new main.SpringboardToSpecificRelease("METEOR@1.0");
  }

  if (pkgVersion.releaseName === null) {
    if (! files.inCheckout()) {
      Console.error(
        "This package was published from a checkout of meteor!",
        "The tool cannot replicate that environment and will not even try.",
        "Please check out meteor at the " +
        "corresponding git commit and try again.");
      process.exit(1);
    }
  } else if (files.inCheckout()) {
    Console.error(
      "This package was published from a built version of meteor, " +
      "but you are running from checkout! Consider running from a " +
      "proper Meteor release with " +
      Console.command("`meteor --release " + pkgVersion.releaseName + "`"),
      "so we can springboard correctly.");
    process.exit(1);
  } else if (pkgVersion.releaseName !== release.current.name) {
    // We are in a built release, and so is the package, but it's a different
    // one. Springboard!
    throw new main.SpringboardToSpecificRelease(pkgVersion.releaseName);
  }

  // OK, either we're running from a checkout and so was the published package,
  // or we're running from the same release as the published package.

  // Download the source to the package.
  var sourceTarball = buildmessage.enterJob("downloading package source", function () {
    return httpHelpers.getUrl({
      url: pkgVersion.source.url,
      encoding: null
    });
  });

  var sourcePath = files.mkdtemp('package-source');
  // XXX check tarballHash!
  files.extractTarGz(sourceTarball, sourcePath);

  // XXX Factor out with packageClient.bundleSource so that we don't
  // have knowledge of the tarball structure in two places.
  var packageDir = files.pathJoin(sourcePath, colonConverter.convert(name));
  if (! files.exists(packageDir)) {
    Console.error('Malformed source tarball');
    return 1;
  }

  var tempProjectDir = files.mkdtemp('meteor-package-arch-build');
  // Copy over a version lock file from the source tarball.
  var versionsFile = files.pathJoin(packageDir, '.versions');
  if (! files.exists(versionsFile)) {
    Console.error(
      "This package has no valid version lock file: are you trying to use " +
      "publish-for-arch on a core package? Publish-for-arch cannot " +
      "guarantee safety. Please use",
      Console.command("'meteor publish --existing-version'"), "instead.");
    process.exit(1);
  }
  files.copyFile(files.pathJoin(packageDir, '.versions'),
                 files.pathJoin(tempProjectDir, '.meteor', 'versions'));

  // Set up the project.
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: tempProjectDir,
    explicitlyAddedLocalPackageDirs: [packageDir],
    // When we publish, we should always include web.cordova unibuilds, even
    // though this temporary directory does not have any cordova platforms
    forceIncludeCordovaUnibuild: true,
    allowIncompatibleUpdate: options['allow-incompatible-update']
  });
  // Just get up to initializing the catalog. We're going to mutate the
  // constraints file a bit before we prepare the build.
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.initializeCatalog();
  });
  projectContext.projectConstraintsFile.addConstraints(
    [utils.parsePackageConstraint(name + "@=" + versionString)]);
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  projectContext.packageMapDelta.displayOnConsole({
    title: "Some package versions changed since this package was published!"
  });

  var isopk = projectContext.isopackCache.getIsopack(name);
  if (! isopk)
    throw Error("didn't build isopack for " + name);

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

  main.captureAndExit(
    "=> Errors while publishing build:",
    "publishing package " + name,
    function () {
      packageClient.createAndPublishBuiltPackage(conn, isopk);
    }
  );

  Console.info('Published ' + name + '@' + versionString + '.');

  refreshOfficialCatalogOrDie();
  return 0;
});

main.registerCommand({
  name: 'publish-release',
  minArgs: 1,
  maxArgs: 1,
  options: {
    'create-track': { type: Boolean },
    'from-checkout': { type: Boolean }
  },
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  try {
    var conn = packageClient.loggedInPackagesConnection();
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }

  var relConf = {};

  // Let's read the json release file. It should, at the very minimum contain
  // the release track name, the release version and some short freeform
  // description.
  try {
    var data = files.readFile(options.args[0], 'utf8');
    relConf = JSON.parse(data);
  } catch (e) {
    Console.error("Could not parse release file: " + e.message);
    return 1;
  }

  // Fill in the order key and any other generated release.json fields.
  main.captureAndExit(
    "=> Errors in release schema:",
    "double-checking release schema",
    function () {
      // Check that the schema is valid -- release.json contains all the
      // required fields, does not contain contradicting information, etc.
      // XXX Check for unknown keys?
      if (! _.has(relConf, 'track')) {
        buildmessage.error(
          "Configuration file must specify release track. (track).");
      }
      if (! _.has(relConf, 'version')) {
        buildmessage.error(
          "Configuration file must specify release version. (version).");
      }
      if (! _.has(relConf, 'description')) {
        buildmessage.error(
          "Configuration file must contain a description (description).");
      } else if (relConf.description.length > 100) {
        buildmessage.error("Description must be under 100 characters.");
      }
      if (! options['from-checkout']) {
        if (! _.has(relConf, 'tool')) {
          buildmessage.error(
            "Configuration file must specify a tool version (tool) unless in " +
              "--from-checkout mode.");
        }
        if (! _.has(relConf, 'packages')) {
          buildmessage.error(
            "Configuration file must specify package versions (packages) " +
              "unless in --from-checkout mode.");
        }
      }

      // If you didn't specify an orderKey and it's compatible with our
      // conventional orderKey generation algorithm, use the algorithm. If you
      // explicitly specify orderKey: null, don't include one.
      if (! _.has(relConf, 'orderKey')) {
        relConf.orderKey = utils.defaultOrderKeyForReleaseVersion(
          relConf.version);
      }
      // This covers both the case of "explicitly specified {orderKey: null}"
      // and "defaultOrderKeyForReleaseVersion returned null".
      if (relConf.orderKey === null) {
        delete relConf.orderKey;
      }

      if (! _.has(relConf, 'orderKey') && relConf.recommended) {
        buildmessage.error("Recommended releases must have order keys.");
      }
      // On the main release track, we can't name the release anything beginning
      // with 0.8 and below, because those are taken for pre-troposphere
      // releases.
      if ((relConf.track === catalog.DEFAULT_TRACK)) {
        var start = relConf.version.slice(0,4);
        if (start === "0.8." || start === "0.7." ||
            start === "0.6." || start === "0.5.") {
          buildmessage.error(
            "It looks like you are trying to publish a pre-package-server " +
            "meteor release. Doing this through the package server is going " +
            "to cause a lot of confusion. Please use the old release process.");
        }
      }
    }
  );

  // Let's check if this is a known release track/ a track to which we are
  // authorized to publish before we do any complicated/long operations, and
  // before we publish its packages.
  if (! options['create-track']) {
    var trackRecord = catalog.official.getReleaseTrack(relConf.track);
    if (!trackRecord) {
      Console.error(
        'There is no release track named ' + relConf.track +
        '. If you are creating a new track, use the --create-track flag.');
      return 1;
    }

    // Check with the server to see if we're organized (we can't due this
    // locally due to organizations).
    if (!packageClient.amIAuthorized(relConf.track,conn,  true)) {
      Console.error('You are not an authorized maintainer of ' +
                    relConf.track + ".");
      Console.error('Only authorized maintainers may publish new versions.');
      return 1;
    }
  }

  // This is sort of a hidden option to just take your entire meteor checkout
  // and make a release out of it. That's what we do now (that's what releases
  // meant pre-0.90), and it is very convenient to do that here.
  //
  // If you have any unpublished packages at new versions in your checkout, this
  // WILL PUBLISH THEM at specified versions. (If you have unpublished changes,
  // including changes to build-time dependencies, but have not incremented the
  // version number, this will use buildmessage to error and exit.)
  //
  // Without any modifications about forks and package names, this particular
  // option is not very useful outside of MDG. Right now, to run this option on
  // a non-MDG fork of meteor, someone would probably need to go through and
  // change the package names to have proper prefixes, etc.
  if (options['from-checkout']) {
    // You must be running from checkout to bundle up your checkout as a release.
    if (!files.inCheckout()) {
      Console.error("Must run from checkout to make release from checkout.");
      return 1;
    };

    // You should not use a release configuration with packages&tool *and* a
    // from checkout option, at least for now. That's potentially confusing
    // (which ones did you mean to use) and makes it likely that you did one of
    // these by accident. So, we will disallow it for now.
    if (relConf.packages || relConf.tool) {
      Console.error(
        "Setting the --from-checkout option will use the tool and packages " +
        "in your meteor checkout. " +
        "Your release configuration file should not contain that information.");
      return 1;
    }

    // Set up a temporary project context and build everything.
    var tempProjectDir = files.mkdtemp('meteor-release-build');
    var projectContext = new projectContextModule.ProjectContext({
      projectDir: tempProjectDir,  // won't have a packages dir, that's OK
      // seriously, we only want checkout packages
      ignorePackageDirsEnvVar: true,
      // When we publish, we should always include web.cordova unibuilds, even
      // though this temporary directory does not have any cordova platforms
      forceIncludeCordovaUnibuild: true
    });

    // Read metadata and initialize catalog.
    main.captureAndExit("=> Errors while building for release:", function () {
      projectContext.initializeCatalog();
    });

    // Ensure that all packages and their tests are built. (We need to build
    // tests so that we can include their sources in source tarballs.)
    var allPackagesWithTests = projectContext.localCatalog.getAllPackageNames();
    var allPackages = projectContext.localCatalog.getAllNonTestPackageNames();
    projectContext.projectConstraintsFile.addConstraints(
      _.map(allPackagesWithTests, function (p) {
        return utils.parsePackageConstraint(p);
      })
    );

    // Build!
    main.captureAndExit("=> Errors while building for release:", function () {
      projectContext.prepareProjectForBuild();
    });
    // No need to display the PackageMapDelta here, since it would include all
    // of the packages!

    relConf.packages = {};
    var toPublish = [];

    main.captureAndExit("=> Errors in release packages:", function () {
      _.each(allPackages, function (packageName) {
        buildmessage.enterJob("checking consistency of " + packageName, function () {
          var packageSource = projectContext.localCatalog.getPackageSource(
            packageName);
          if (! packageSource)
            throw Error("no PackageSource for built package " + packageName);

          if (! packageSource.versionExplicitlyProvided) {
            buildmessage.error(
              "A version must be specified for the package. Set it with " +
                "Package.describe.");
            return;
          }

          // Let's get the server version that this local package is
          // overwriting. If such a version exists, we will need to make sure
          // that the contents are the same.
          var oldVersionRecord = catalog.official.getVersion(
            packageName, packageSource.version);

          // Include this package in our release.
          relConf.packages[packageName] = packageSource.version;

          // If there is no old version, then we need to publish this package.
          if (! oldVersionRecord) {
            // We are going to check if we are publishing an official
            // release. If this is an experimental or pre-release, then we are
            // not ready to commit to these package semver versions either. Any
            // packages that we should publish as part of this release should
            // have a -(something) at the end.
            var newVersion = packageSource.version;
            if (! relConf.official && newVersion.split("-").length < 2) {
              buildmessage.error(
                "It looks like you are building an experimental release or " +
                  "pre-release. Any packages we publish here should have an " +
                  "pre-release identifier at the end (eg, 1.0.0-dev). If " +
                  "this is an official release, please set official to true " +
                  "in the release configuration file.");
              return;
            }
            toPublish.push(packageName);
            Console.info("Will publish new version for " + packageName);
            return;
          } else {
            var isopk = projectContext.isopackCache.getIsopack(packageName);
            if (! isopk)
              throw Error("no isopack for " + packageName);

            var existingBuild =
                  catalog.official.getBuildWithPreciseBuildArchitectures(
                    oldVersionRecord, isopk.buildArchitectures());

            // If the version number mentioned in package.js exists, but there's
            // no build of this architecture, then either the old version was
            // only semi-published, or you've added some platform-specific
            // dependencies but haven't bumped the version number yet; either
            // way, you should probably bump the version number.
            var somethingChanged = ! existingBuild;

            if (!somethingChanged) {
              // Save the isopack, just to get its hash.
              var bundleBuildResult = packageClient.bundleBuild(isopk);
              if (bundleBuildResult.treeHash !== existingBuild.build.treeHash) {
                somethingChanged = true;
              }
            }

            if (somethingChanged) {
              buildmessage.error(
                "Something changed in package " + packageName + "@" +
                  isopk.version + ". Please upgrade its version number.");
            }
          }
        });
      });
    });

    // We now have an object of packages that have new versions on disk that
    // don't exist in the server catalog. Publish them.
    var unfinishedBuilds = {};
    _.each(toPublish, function (packageName) {
      main.captureAndExit(
        "=> Errors while publishing:",
        "publishing package " + packageName,
        function () {
          var isopk = projectContext.isopackCache.getIsopack(packageName);
          if (! isopk)
            throw Error("no isopack for " + packageName);
          var packageSource = projectContext.localCatalog.getPackageSource(
            packageName);
          if (! packageSource)
            throw Error("no PackageSource for built package " + packageName);

          var binary = isopk.platformSpecific();
          packageClient.publishPackage({
            projectContext: projectContext,
            packageSource: packageSource,
            connection: conn,
            new: ! catalog.official.getPackage(packageName),
            doNotPublishBuild: binary
          });
          if (buildmessage.jobHasMessages())
            return;

          Console.info(
            'Published ' + packageName + '@' + packageSource.version + '.');

          if (binary) {
            unfinishedBuilds[packageName] = packageSource.version;
          }
        });
    });

    // Set the remaining release information. For now, when we publish from
    // checkout, we always set 'meteor-tool' as the tool. We don't include the
    // tool in the packages list.
    relConf.tool="meteor-tool@" + relConf.packages["meteor-tool"];
    delete relConf.packages["meteor-tool"];
  }

  main.captureAndExit(
    "=> Errors while publishing release:",
    "publishing release",
    function () {
      // Create the new track, if we have been told to.
      if (options['create-track']) {
        // XXX maybe this job title should be left on the screen too?  some sort
        // of enterJob/progress option that lets you do that?
        buildmessage.enterJob("creating a new release track", function () {
          packageClient.callPackageServerBM(
            conn, 'createReleaseTrack', { name: relConf.track } );
        });
        if (buildmessage.jobHasMessages())
          return;
      }

      buildmessage.enterJob("creating a new release version", function () {
        var record = {
          track: relConf.track,
          version: relConf.version,
          orderKey: relConf.orderKey,
          description: relConf.description,
          recommended: !!relConf.recommended,
          tool: relConf.tool,
          packages: relConf.packages
        };

        var uploadInfo;
        if (relConf.patchFrom) {
          uploadInfo = packageClient.callPackageServerBM(
            conn, 'createPatchReleaseVersion', record, relConf.patchFrom);
        } else {
          uploadInfo = packageClient.callPackageServerBM(
            conn, 'createReleaseVersion', record);
        }
      });
    }
  );

  // Learn about it.
  refreshOfficialCatalogOrDie();
  Console.info("Done creating " + relConf.track  + "@" + relConf.version + "!");
  Console.info();

  if (options['from-checkout']) {
    // XXX maybe should discourage publishing if git status says we're dirty?
    var gitTag = "release/" + relConf.track  + "@" + relConf.version;
    if (config.getPackageServerFilePrefix() !== 'packages') {
      // Only make a git tag if we're on the default branch.
      Console.info("Skipping git tag: not using the main package server.");
    } else if (gitTag.indexOf(':') !== -1) {
      // XXX could run `git check-ref-format --allow-onelevel $gitTag` like we
      //     used to, instead of this simple check
      // XXX could convert : to / ?
      Console.info("Skipping git tag: bad format for git.");
    } else {
      Console.info("Creating git tag " + gitTag);
      files.runGitInCheckout('tag', gitTag);
      var fail = false;
      try {
        Console.info(
          "Pushing git tag (this should fail if you are not from MDG)");
        files.runGitInCheckout('push', 'git@github.com:meteor/meteor.git',
                             'refs/tags/' + gitTag);
      } catch (err) {
        Console.error(
          "Failed to push git tag. Please push git tag manually!");
        fail = true;
      }
    }

    // We need to warn the user that we didn't publish some of their
    // packages. Unlike publish, this is advanced functionality, so the user
    // should be familiar with the concept.
    if (! _.isEmpty(unfinishedBuilds)) {
      Console.warn();
      Console.labelWarn(
        "Some packages contain binary dependencies.");
      Console.warn(
          "Builds have not been published for the following packages:");
      _.each(unfinishedBuilds, function (version, name) {
        Console.warn(name + "@" + version);
      });
      // Note: we don't actually enforce the proper build machine thing. You
      // can't use publish-for-arch for meteor-tool, for example, you need to
      // use publish --existing-version and to do it from checkout. Setting that
      // up on a build machine for a one-off experimental release could be a
      // pain. In that case, I guess, you can just run publish
      // --existing-version: presumably you don't care about compatibility
      // etc. If it is an official release, you ought to use a build machine
      // though.
      Console.warn(
        "Please publish the builds separately, from a proper build machine.");
    }
  }

  return 0;
});

///////////////////////////////////////////////////////////////////////////////
// list
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'list',
  requiresApp: true,
  options: {
    'allow-incompatible-update': { type: Boolean }
  },
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: true })
}, function (options) {
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    allowIncompatibleUpdate: options['allow-incompatible-update']
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  // No need to display the PackageMapDelta here, since we're about to list all
  // of the packages anyway!


  var items = [];
  var newVersionsAvailable = false;
  var anyBuiltLocally = false;

  // Iterate over packages that are used directly by this app (not indirect
  // dependencies).
  projectContext.projectConstraintsFile.eachConstraint(function (constraint) {
    var packageName = constraint.package;
    var mapInfo = projectContext.packageMap.getInfo(packageName);
    if (! mapInfo)
      throw Error("no version for used package " + packageName);
    var versionRecord = projectContext.projectCatalog.getVersion(
      packageName, mapInfo.version);
    if (! versionRecord) {
      throw Error("no version record for " + packageName + "@" +
                  mapInfo.version);
    }

    var versionAddendum = " ";
    if (mapInfo.kind === 'local') {
      versionAddendum = "+";
      anyBuiltLocally = true;
    } else if (mapInfo.kind === 'versioned') {
      // Check to see if there are later versions available.
      // If we are not using an rc for this package, then we are not going to
      // update to an rc. But if we are using a pre-release version, then we
      // care about other pre-release versions, and might want to update to a
      // newer one.
      var latest;
      if (/-/.test(mapInfo.version)) {
        latest = catalog.official.getLatestVersion(packageName);
      } else {
        latest = catalog.official.getLatestMainlineVersion(packageName);
      }
      if (! latest) {
        // Shouldn't happen: we've chosen a published version of this package,
        // so there has to be at least one in our database!
        throw Error("no latest record for package where we have a version? " +
                    packageName);
      }
      if (mapInfo.version !== latest.version &&
          // If we're currently running a prerelease, "latest" may be older than
          // what we're at, so don't tell us we're outdated!
          packageVersionParser.lessThan(mapInfo.version, latest.version)) {
        versionAddendum = "*";
        newVersionsAvailable = true;
      }
    } else {
      throw Error("unknown kind " + mapInfo.kind);
    }
    var description = mapInfo.version + versionAddendum;
    if (versionRecord.description) {
      description += " " + versionRecord.description;
    }
    items.push({ name: packageName, description: description });
  });

  // Append extra information about special packages such as Cordova plugins
  // to the list.
  _.each(
    projectContext.cordovaPluginsFile.getPluginVersions(),
    function (version, name) {
      items.push({ name: 'cordova:' + name, description: version });
    }
  );

  utils.printPackageList(items);

  if (newVersionsAvailable) {
    Console.info();
    Console.info(
      "New versions of these packages are available! Run",
      Console.command("'meteor update'"), "to try to update those",
      "packages to their latest versions. If your packages cannot be",
      "updated further, try typing",
      Console.command("`meteor add <package>@<newVersion>`"),
      "to see more information.",
      Console.options({ bulletPoint: "* " }));
  }
  if (anyBuiltLocally) {
    Console.info();
    Console.info(
      "These packages are built locally from source.",
      Console.options({ bulletPoint: "+ " }));
  }
  return 0;
});


///////////////////////////////////////////////////////////////////////////////
// update
///////////////////////////////////////////////////////////////////////////////

// Returns 0 if the operation went OK -- either we updated to a new release, or
// decided not to with good reason. Returns something other than 0, if it is not
// safe to proceed (ex: our release track is fundamentally unsafe or there is
// weird catalog corruption).
var maybeUpdateRelease = function (options) {
  // We are only updating packages, so we are not updating the release.
  if (options["packages-only"]) {
     return 0;
  }

  // We are running from checkout, so we are not updating the release.
  if (release.current && release.current.isCheckout()) {
    Console.error(
      "You are running Meteor from a checkout, so we cannot update",
      "the Meteor release. Checking to see if we can update your packages.");
    return 0;
  }

  // Looks like we are going to have to update the release. First, let's figure
  // out the release track we'll end up on --- either because it's
  // the explicitly specified (with --release) track; or because we didn't
  // specify a release and it's the app's current release (if we're in an app
  // dir), since non-forced updates don't change the track.

  // XXX better error checking on release.current.name
  // XXX add a method to release.current.
  var releaseTrack = release.current ?
        release.current.getReleaseTrack() : catalog.DEFAULT_TRACK;

  // Unless --release was passed (in which case we ought to already have
  // springboarded to that release), go get the latest release and switch to
  // it. (We already know what the latest release is because we refreshed the
  // catalog above.)  Note that after springboarding, we will hit this again.
  // However, the override that's done by SpringboardToLatestRelease also sets
  // release.forced (although it does not set release.explicit), so we won't
  // double-springboard.  (We might miss an super recently published release,
  // but that's probably OK.)
  if (! release.forced) {
    var latestRelease = release.latestKnown(releaseTrack);

    // Are we on some track without ANY recommended releases at all,
    // and the user ran 'meteor update' without specifying a release? We
    // really can't do much here.
    if (!latestRelease) {
      Console.error(
        "There are no recommended releases on release track " +
          releaseTrack + ".");
      return 1;
    }
    if (! release.current || release.current.name !== latestRelease) {
      // The user asked for the latest release (well, they "asked for it" by not
      // passing --release). We're not currently running the latest release on
      // this track (we may have even just learned about it). #UpdateSpringboard
      throw new main.SpringboardToLatestRelease(releaseTrack);
    }
  }

  // At this point we should have a release. (If we didn't to start
  // with, #UpdateSpringboard fixed that.) And it can't be a checkout,
  // because we checked for that at the very beginning.
  if (! release.current || ! release.current.isProperRelease())
    throw new Error("don't have a proper release?");

  // If we're not in an app, then we're basically done. The only thing left to
  // do is print out some messages explaining what happened (and advising the
  // user to run update from an app).
  if (! options.appDir) {
    if (release.forced) {
      // We get here if:
      // 1) the user ran 'meteor update' and we found a new version
      // 2) the user ran 'meteor update --release xyz' (regardless of
      //    whether we found a new release)
      //
      // In case (1), we downloaded and installed the update and then
      // we springboarded (at #UpdateSpringboard above), causing
      // $METEOR_SPRINGBOARD_RELEASE to be true.
      // XXX probably should have a better interface than looking directly
      //     at the env var here
      //
      // In case (2), we downloaded, installed, and springboarded to
      // the requested release in the initialization code, before the
      // command even ran. They could equivalently have run 'meteor
      // help --release xyz'.
      Console.info(
        "Installed. Run",
        Console.command(
          "'meteor update --release " +
            release.current.getDisplayName({ noPrefix: true }) + "'"),
        "inside of a particular project directory to update that project to",
        release.current.getDisplayName() + ".");
    } else {
      // We get here if the user ran 'meteor update' and we didn't
      // find a new version.
      Console.info(
        "The latest version of Meteor, " + release.current.getReleaseVersion() +
        ", is already installed on this computer. Run " +
        Console.command("'meteor update'") + " inside of a particular " +
        "project directory to update that project to " +
        release.current.getDisplayName());
    }
    return 0;
  }

  // Otherwise, we have to upgrade the app too, if the release changed.  Read in
  // the project metadata files.  (Don't resolve constraints yet --- if the
  // current constraints don't resolve but we can update to a place where they
  // do, that's great!)
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    alwaysWritePackageMap: true,
    allowIncompatibleUpdate: true // disregard `.meteor/versions` if necessary
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.readProjectMetadata();
  });

  if (projectContext.releaseFile.fullReleaseName === release.current.name) {
    // release.explicit here means that the user actually typed `--release FOO`,
    // so they weren't trying to go to the latest release. (This is different
    // from release.forced, which might be set due to the
    // SpringboardToLatestRelease above.)
    var maybeTheLatestRelease = release.explicit ? "" : ", the latest release";
    Console.info(
      "This project is already at " +
      release.current.getDisplayName() + maybeTheLatestRelease + ".");
    return 0;
  }

  // XXX did we have to change some package versions? we should probably
  //     mention that fact.
  // XXX error handling.

  // Figuring out which release to use to update the app is slightly more
  // complicated, because we have to run the constraint solver. So, we need to
  // try multiple releases, defined by the various options passed in.
  var releaseVersionsToTry;
  if (options.patch) {
    // Can't make a patch update if you are not running from a current
    // release. In fact, you are doing something wrong, so we should tell you
    // to stop.
    if (! projectContext.releaseFile.normalReleaseSpecified()) {
      Console.error(
        "Cannot patch update unless a release is set.");
      return 1;
    }
    var record = catalog.official.getReleaseVersion(
      projectContext.releaseFile.releaseTrack,
      projectContext.releaseFile.releaseVersion);
    if (!record) {
      Console.error(
        "Cannot update to a patch release from an old release.");
      return 1;
    }
    var updateTo = record.patchReleaseVersion;
    if (!updateTo) {
      Console.error(
        "You are at the latest patch version.");
      return 0;
    }
    var patchRecord = catalog.official.getReleaseVersion(
      projectContext.releaseFile.releaseTrack, updateTo);
    // It looks like you are not at the latest patch version,
    // technically. But, in practice, we cannot update you to the latest patch
    // version because something went wrong. For example, we can't find the
    // record for your patch version (probably some sync
    // failure). Alternatively, maybe we put out a patch release and found a
    // bug in it -- since we tell you to always run update --patch, we should
    // not try to patch you to an unfriendly release. So, either way, as far
    // as we are concerned you are at the 'latest patch version'
    if (!patchRecord || !patchRecord.recommended ) {
      Console.error("You are at the latest patch version.");
      return 0;
    }
    // Great, we found a patch version. You can only have one latest patch for
    // a string of releases, so there is just one release to try.
    releaseVersionsToTry = [updateTo];
  } else if (release.explicit) {
    // You have explicitly specified a release, and we have springboarded to
    // it. So, we will use that release to update you to itself, if we can.
    releaseVersionsToTry = [release.current.getReleaseVersion()];
  } else {
    // We are not doing a patch update, or a specific release update, so we need
    // to try all recommended releases on our track, whose order key is greater
    // than the app's.
    var appReleaseInfo = catalog.official.getReleaseVersion(
      projectContext.releaseFile.releaseTrack,
      projectContext.releaseFile.releaseVersion);
    var appOrderKey = (appReleaseInfo && appReleaseInfo.orderKey) || null;

    releaseVersionsToTry = catalog.official.getSortedRecommendedReleaseVersions(
      projectContext.releaseFile.releaseTrack, appOrderKey);
    if (! releaseVersionsToTry.length) {
      // We could not find any releases newer than the one that we are on, on
      // that track, so we are done.
      Console.info(
        "This project is already at " +
        Console.noWrap(projectContext.releaseFile.displayReleaseName) +
        ", which is newer than the latest release.");
      return 0;
    }
  }

  var solutionReleaseRecord = null;
  var solutionReleaseVersion = _.find(releaseVersionsToTry, function (versionToTry) {
    var releaseRecord = catalog.official.getReleaseVersion(
      releaseTrack, versionToTry);
    if (!releaseRecord)
      throw Error("missing release record?");

    // Reset the project context and pretend we're using the potential release.
    projectContext.reset({ releaseForConstraints: releaseRecord });
    var messages = buildmessage.capture(function () {
      projectContext.resolveConstraints();
    });
    if (messages.hasMessages()) {
      // Nope, this release didn't work.
      Console.debug(
        "Update to release", releaseTrack + "@" + versionToTry,
        "is impossible:");
      Console.debug(messages.formatMessages());
      return false;
    }

    // Yay, it worked!
    solutionReleaseRecord = releaseRecord;
    return true;
  });

  var newerAvailable = false;
  if (! solutionReleaseVersion) {
    Console.info(
      "This project is at the latest release which is compatible with your " +
      "current package constraints.");
    return 0;
  } else if (solutionReleaseVersion !== releaseVersionsToTry[0]) {
    newerAvailable = true;
  }

  var solutionReleaseName = releaseTrack + '@' + solutionReleaseVersion;

  // We could at this point springboard to solutionRelease (which is no newer
  // than the release we are currently running), but there's no super-clear
  // advantage to this yet. The main reason might be if we decide to delete some
  // backward-compatibility code which knows how to deal with an older release,
  // but if we actually do that, we can change this code to add the extra
  // springboard at that time.
  var upgraders = require('./upgraders.js');
  var upgradersToRun = upgraders.upgradersToRun(projectContext);

  // Download and build packages and write the new versions to .meteor/versions.
  // XXX It's a little weird that we do a full preparation for build
  //     (downloading packages, building packages, etc) when we might be about
  //     to upgrade packages and have to do it again. Maybe we shouldn't? Note
  //     that if we change this, that changes the upgraders interface, which
  //     expects a projectContext that is fully prepared for build.
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  // Write the new release to .meteor/release.
  projectContext.releaseFile.write(solutionReleaseName);
  projectContext.packageMapDelta.displayOnConsole({
    title: ("Changes to your project's package version selections from " +
            "updating the release:")
  });

  Console.info(files.pathBasename(options.appDir) + ": updated to " +
               projectContext.releaseFile.displayReleaseName + ".");
  if (newerAvailable) {
    Console.info(
      "(Newer releases are available but are not compatible with your " +
      "current package constraints.)");
  }

  // Now run the upgraders.
  // XXX should we also run upgraders on other random commands, in case there
  // was a crash after changing .meteor/release but before running them?
  _.each(upgradersToRun, function (upgrader) {
    upgraders.runUpgrader(projectContext, upgrader);
    projectContext.finishedUpgraders.appendUpgraders([upgrader]);
  });

  // We are done, and we should pass the release that we upgraded to, to the
  // user.
  return 0;
};

main.registerCommand({
  name: 'update',
  options: {
    patch: { type: Boolean },
    "packages-only": { type: Boolean },
    "allow-incompatible-update": { type: Boolean }
  },
  // We have to be able to work without a release, since 'meteor
  // update' is how you fix apps that don't have a release.
  requiresRelease: false,
  minArgs: 0,
  maxArgs: Infinity,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: true })
}, function (options) {
  // If you are specifying packages individually, you probably don't want to
  // update the release.
  if (options.args.length > 0) {
    options["packages-only"] = true;
  }

  // Some basic checks to make sure that this command is being used correctly.
  if (options["packages-only"] && options["patch"]) {
    Console.error(
      "The --patch option only applies to the release, not packages.");
    return 1;
  }

  if (release.explicit && options["patch"]) {
    Console.error("You cannot patch update to a specific release.");
    return 1;
  }

  var releaseUpdateStatus = maybeUpdateRelease(options);
  // If we encountered an error and cannot proceed, return.
  if (releaseUpdateStatus !== 0) {
    return releaseUpdateStatus;
  }

  // The only thing left to do is update packages, and we don't update packages
  // if we are making a patch update, updating specifically with a --release, or
  // running outside a package directory. So, we are done, return.
  if (options['patch'] || release.explicit || !options.appDir) {
    return 0;
  }

  // Start a new project context and read in the project's release and other
  // metadata. (We also want to make sure that we write the package map when
  // we're done even if we're not on the matching release!)
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    alwaysWritePackageMap: true,
    allowIncompatibleUpdate: options["allow-incompatible-update"]
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.readProjectMetadata();
  });

  // If no packages have been specified, then we will send in a request to
  // update all direct dependencies. If a specific list of packages has been
  // specified, then only upgrade those.
  var upgradePackageNames = [];
  // If no packages have been specified (`meteor update` with no positional
  // args), take patches to indirect dependencies.
  var upgradeIndirectDepPatchVersions = false;
  if (options.args.length === 0) {
    projectContext.projectConstraintsFile.eachConstraint(function (constraint) {
      upgradePackageNames.push(constraint.package);
    });
    upgradeIndirectDepPatchVersions = true;
  } else {
    upgradePackageNames = options.args;
  }
  // We want to use the project's release for constraints even if we are
  // currently running a newer release (eg if we ran 'meteor update --patch' and
  // updated to an older patch release).  (If the project has release 'none'
  // because this is just 'updating packages', this can be null.)
  var releaseRecordForConstraints = null;
  if (projectContext.releaseFile.normalReleaseSpecified()) {
    releaseRecordForConstraints = catalog.official.getReleaseVersion(
      projectContext.releaseFile.releaseTrack,
      projectContext.releaseFile.releaseVersion);
    if (! releaseRecordForConstraints) {
      throw Error("unknown release " +
                  projectContext.releaseFile.displayReleaseName);
    }
  }

  // Try to resolve constraints, allowing the given packages to be upgraded.
  projectContext.reset({
    releaseForConstraints: releaseRecordForConstraints,
    upgradePackageNames: upgradePackageNames,
    upgradeIndirectDepPatchVersions: upgradeIndirectDepPatchVersions
  });
  main.captureAndExit(
    "=> Errors while upgrading packages:", "upgrading packages", function () {
      projectContext.resolveConstraints();
      if (buildmessage.jobHasMessages())
        return;

      // If the user explicitly mentioned some packages to upgrade, they must
      // actually end up in our solution!
      _.each(options.args, function (packageName) {
        if (! projectContext.packageMap.getInfo(packageName)) {
          buildmessage.error(packageName + ': package is not in the project');
        }
      });
      if (buildmessage.jobHasMessages())
        return;

      // Finish preparing the project.
      projectContext.prepareProjectForBuild();
    }
  );

  if (projectContext.packageMapDelta.hasChanges()) {
    projectContext.packageMapDelta.displayOnConsole({
      title: ("Changes to your project's package version selections from " +
              "updating package versions:")
    });
  } else if (options.args.length) {
    Console.info(
      "The specified packages are at their latest compatible versions.");
  } else {
    Console.info(
      "Your top-level dependencies are at their latest compatible versions.");
  }

  if (! options.args.length) {
    // Generate and print info about what is NOT at the latest version.
    var topLevelPkgSet = {};
    projectContext.projectConstraintsFile.eachConstraint(function (constraint) {
      topLevelPkgSet[constraint.package] = true;
    });
    var nonlatestDirectDeps = [];
    var nonlatestIndirectDeps = [];
    projectContext.packageMap.eachPackage(function (name, info) {
      var selectedVersion = info.version;
      var catalog = projectContext.projectCatalog;
      var latestVersion = catalog.getLatestMainlineVersion(name).version;
      if (selectedVersion !== latestVersion) {
        var rec = { name: name, selectedVersion: selectedVersion,
                    latestVersion: latestVersion };
        if (_.has(topLevelPkgSet, name)) {
          nonlatestDirectDeps.push(rec);
        } else {
          nonlatestIndirectDeps.push(rec);
        }
      }
    });
    var printItem = function (rec) {
      Console.info(" * " + rec.name + " " + rec.selectedVersion +
                   " (" + rec.latestVersion + " is available)");
    };
    if (nonlatestDirectDeps.length) {
      Console.info("\nThe following top-level dependencies were not updated " +
                   "to the very latest version available:");
      _.each(nonlatestDirectDeps, printItem);
    }
    if (nonlatestIndirectDeps.length) {
      Console.info("\nNewer versions of the following indirect dependencies" +
                   " are available:");
      _.each(nonlatestIndirectDeps, printItem);
      Console.info(
        "To update one or more of these packages, pass their names to " +
          "`meteor update`.");
    }
  }
});

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

// For testing upgraders during development.
main.registerCommand({
  name: 'admin run-upgrader',
  hidden: true,
  minArgs: 1,
  maxArgs: 1,
  requiresApp: true,
  catalogRefresh: new catalog.Refresh.Never(),
  'allow-incompatible-update': { type: Boolean }
}, function (options) {
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    allowIncompatibleUpdate: options['allow-incompatible-update']
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    projectContext.prepareProjectForBuild();
  });
  projectContext.packageMapDelta.displayOnConsole();

  var upgrader = options.args[0];

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

///////////////////////////////////////////////////////////////////////////////
// admin run-background-updater
///////////////////////////////////////////////////////////////////////////////

// For testing the background updater during development.
main.registerCommand({
  name: 'admin run-background-updater',
  hidden: true,
  catalogRefresh: new catalog.Refresh.Never()
}, function (options) {
  updater.tryToDownloadUpdate({
    showBanner: true,
    printErrors: true
  });
});

///////////////////////////////////////////////////////////////////////////////
// admin wipe-all-packages
///////////////////////////////////////////////////////////////////////////////

// For testing wipeAllPackages during development
main.registerCommand({
  name: 'admin wipe-all-packages',
  hidden: true,
  catalogRefresh: new catalog.Refresh.Never()
}, function (options) {
  tropohouse.default.wipeAllPackages();
});

///////////////////////////////////////////////////////////////////////////////
// admin check-package-versions
///////////////////////////////////////////////////////////////////////////////

// Run before publish-release --from-checkout to make sure that all of our
// version numbers are up to date
main.registerCommand({
  name: 'admin check-package-versions',
  hidden: true,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  if (!files.inCheckout()) {
    Console.error("Must run from checkout.");
    return 1;
  };

  // Set up a temporary project context and build everything.
  var tempProjectDir = files.mkdtemp('meteor-release-build');
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: tempProjectDir,  // won't have a packages dir, that's OK
    // seriously, we only want checkout packages
    ignorePackageDirsEnvVar: true,
    // When we publish, we should always include web.cordova unibuilds, even
    // though this temporary directory does not have any cordova platforms
    forceIncludeCordovaUnibuild: true
  });

  // Read metadata and initialize catalog.
  main.captureAndExit("=> Errors while building for release:", function () {
    projectContext.initializeCatalog();
  });

  var allPackages = projectContext.localCatalog.getAllNonTestPackageNames();

  Console.info("Listing packages where the checkout version doesn't match the",
    "latest version on the package server.");

  _.each(allPackages, function (packageName) {
    var checkoutVersion = projectContext.localCatalog.getLatestVersion(packageName).version;
    var remoteLatestVersion = catalog.official.getLatestVersion(packageName).version;

    if (checkoutVersion !== remoteLatestVersion) {
      Console.info(packageName, checkoutVersion, remoteLatestVersion);
    }
  });
});

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

main.registerCommand({
  name: 'add',
  options: {
    "allow-incompatible-update": { type: Boolean }
  },
  minArgs: 1,
  maxArgs: Infinity,
  requiresApp: true,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: true })
}, function (options) {
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    allowIncompatibleUpdate: options["allow-incompatible-update"]
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    // We're just reading metadata here --- we're not going to resolve
    // constraints until after we've made our changes.
    projectContext.initializeCatalog();
  });

  var exitCode = 0;

  var filteredPackages = cordova.filterPackages(options.args);
  var pluginsToAdd = filteredPackages.plugins;

  if (pluginsToAdd.length) {
    var plugins = projectContext.cordovaPluginsFile.getPluginVersions();
    var changed = false;
    _.each(pluginsToAdd, function (pluginSpec) {
      var parts = pluginSpec.split('@');
      if (parts.length !== 2) {
        Console.error(
          pluginSpec + ': exact version or tarball url is required');
        exitCode = 1;
      } else if (! utils.isExactVersion(parts[1])) {
        Console.error(
          "Must declare exact version of dependency: " + pluginSpec);
        exitCode = 1;
      } else {
        plugins[parts[0]] = parts[1];
        changed = true;
        Console.info("added cordova plugin " + parts[0]);
      }
    });
    changed && projectContext.cordovaPluginsFile.write(plugins);
  }

  var args = filteredPackages.rest;

  if (_.isEmpty(args))
    return exitCode;

  // Messages that we should print if we make any changes, but that don't count
  // as errors.
  var infoMessages = [];
  var constraintsToAdd = [];
  // 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.
  var messages = buildmessage.capture(function () {
    _.each(args, function (packageReq) {
      buildmessage.enterJob("adding package " + packageReq, function () {
        var constraint = utils.parsePackageConstraint(packageReq, {
          useBuildmessage: true
        });
        if (buildmessage.jobHasMessages())
          return;

        // It's OK to make errors based on looking at the catalog, because this
        // is a OnceAtStart command.
        var packageRecord = projectContext.projectCatalog.getPackage(
          constraint.package);
        if (! packageRecord) {
          buildmessage.error("no such package");
          return;
        }

        _.each(constraint.versionConstraint.alternatives, function (subConstr) {
          if (subConstr.versionString === null)
            return;
          // Figure out if this version exists either in the official catalog or
          // the local catalog. (This isn't the same as using the combined
          // catalog, since it's OK to type "meteor add foo@1.0.0" if the local
          // package is 1.1.0 as long as 1.0.0 exists.)
          var versionRecord = projectContext.localCatalog.getVersion(
            constraint.package, subConstr.versionString);
          if (! versionRecord) {
            // XXX #2846 here's an example of something that might require a
            // refresh
            versionRecord = catalog.official.getVersion(
              constraint.package, subConstr.versionString);
          }
          if (! versionRecord) {
            buildmessage.error("no such version " + constraint.package + "@" +
                               subConstr.versionString);
          }
        });
        if (buildmessage.jobHasMessages())
          return;

        var current = projectContext.projectConstraintsFile.getConstraint(
          constraint.package);

        // Check that the constraint is new. If we are already using the package
        // at the same constraint in the app, we will log an info message later
        // (if there are no other errors), but don't fail. Rejecting the entire
        // command because a part of it is a no-op is confusing.
        if (! current) {
          constraintsToAdd.push(constraint);
        } else if (! current.constraintString &&
                   ! constraint.constraintString) {
          infoMessages.push(
            constraint.package +
              " without a version constraint has already been added.");
        } else if (current.constraintString === constraint.constraintString) {
          infoMessages.push(
            constraint.package + " with version constraint " +
              constraint.constraintString + " has already been added.");
        } else {
          // We are changing an existing constraint.
          if (current.constraintString) {
            infoMessages.push(
              "Currently using " + constraint.package +
                " with version constraint " + current.constraintString + ".");
          } else {
            infoMessages.push(
              "Currently using " +  constraint.package +
                " without any version constraint.");
          }
          if (constraint.constraintString) {
            infoMessages.push("The version constraint will be changed to " +
                              constraint.constraintString + ".");
          } else {
            infoMessages.push("The version constraint will be removed.");
          }
          constraintsToAdd.push(constraint);
        }
      });
    });
  });
  if (messages.hasMessages()) {
    Console.arrowError("Errors while parsing arguments:", 1);
    Console.printMessages(messages);
    utils.explainIfRefreshFailed();  // this is why we're not using captureAndExit
    return 1;
  }

  projectContext.projectConstraintsFile.addConstraints(constraintsToAdd);

  // Run the constraint solver, download packages, etc.
  messages = buildmessage.capture(function () {
    projectContext.prepareProjectForBuild();
  });
  if (messages.hasMessages()) {
    Console.arrowError("Errors while adding packages:", 1);
    Console.printMessages(messages);
    utils.explainIfRefreshFailed();  // this is why we're not using captureAndExit
    return 1;
  }

  _.each(infoMessages, function (message) {
    Console.info(message);
  });
  projectContext.packageMapDelta.displayOnConsole();

  // Show descriptions of directly added packages.
  Console.info();
  _.each(constraintsToAdd, function (constraint) {
    var version = projectContext.packageMap.getInfo(constraint.package).version;
    var versionRecord = projectContext.projectCatalog.getVersion(
      constraint.package, version);
    Console.info(
      constraint.package +
        (versionRecord.description ? (": " + versionRecord.description) : ""));
  });

  return exitCode;
});


///////////////////////////////////////////////////////////////////////////////
// remove
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
  name: 'remove',
  options: {
    "allow-incompatible-update": { type: Boolean }
  },
  minArgs: 1,
  maxArgs: Infinity,
  requiresApp: true,
  catalogRefresh: new catalog.Refresh.Never()
}, function (options) {
  var projectContext = new projectContextModule.ProjectContext({
    projectDir: options.appDir,
    allowIncompatibleUpdate: options["allow-incompatible-update"]
  });
  main.captureAndExit("=> Errors while initializing project:", function () {
    // We're just reading metadata here --- we're not going to resolve
    // constraints until after we've made our changes.
    projectContext.readProjectMetadata();
  });

  // Special case on reserved package namespaces, such as 'cordova'
  var filteredPackages = cordova.filterPackages(options.args);
  var pluginsToRemove = filteredPackages.plugins;

  var exitCode = 0;

  // Update the plugins list
  if (pluginsToRemove.length) {
    var plugins = projectContext.cordovaPluginsFile.getPluginVersions();
    var changed = false;
    _.each(pluginsToRemove, function (pluginName) {
      if (/@/.test(pluginName)) {
        Console.error(pluginName + ": do not specify version constraints.");
        exitCode = 1;
      } else if (_.has(plugins, pluginName)) {
        delete plugins[pluginName];
        Console.info("removed cordova plugin " + pluginName);
        changed = true;
      } else {
        Console.error("cordova plugin " + pluginName +
                      " is not in this project.");
        exitCode = 1;
      }
    });
    changed && projectContext.cordovaPluginsFile.write(plugins);
  }

  var args = filteredPackages.rest;

  if (_.isEmpty(args))
    return exitCode;

  // 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.
  var packagesToRemove = [];
  _.each(args, function (package) {
    if (/@/.test(package)) {
      Console.error(package + ": do not specify version constraints.");
      exitCode = 1;
    } else if (! projectContext.projectConstraintsFile.getConstraint(package)) {
      // Check that we are using the package. We don't check if the package
      // exists. You should be able to remove non-existent packages.
      Console.error(package  + " is not in this project.");
      exitCode = 1;
    } else {
      packagesToRemove.push(package);
    }
  });
  if (! packagesToRemove.length)
    return exitCode;

  // Remove the packages from the in-memory representation of .meteor/packages.
  projectContext.projectConstraintsFile.removePackages(packagesToRemove);

  // Run the constraint solver, rebuild local packages, etc. This will write
  // our changes to .meteor/packages if it succeeds.
  main.captureAndExit("=> Errors after removing packages", function () {
    projectContext.prepareProjectForBuild();
  });
  projectContext.packageMapDelta.displayOnConsole();

  // 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 we do this in addition to dislpaying the PackageMapDelta.
  _.each(packagesToRemove, function (packageName) {
    Console.info(packageName + ": removed dependency");
  });

  return exitCode;
});


///////////////////////////////////////////////////////////////////////////////
// refresh
///////////////////////////////////////////////////////////////////////////////

main.registerCommand({
  name: 'refresh',
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  // We already did it!
  return 0;
});


///////////////////////////////////////////////////////////////////////////////
// admin
///////////////////////////////////////////////////////////////////////////////

// For admin commands, at least in preview0.90, we can be kind of lazy and not bother
// to pre-check if the command will succeed client-side. That's because we both
// don't expect them to be called often and don't expect them to be called by
// inexperienced users, so waiting to get rejected by the server is OK.

main.registerCommand({
  name: 'admin maintainers',
  minArgs: 1,
  maxArgs: 1,
  options: {
    add: { type: String, short: "a" },
    remove: { type: String, short: "r" },
    list: { type: Boolean }
  },
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  var name = options.args[0];

  // Yay, checking that options are correct.
  if (options.add && options.remove) {
    Console.error(
      "Sorry, you can only add or remove one user at a time.");
    return 1;
  }
  if ((options.add || options.remove) && options.list) {
    Console.error(
      "Sorry, you can't change the users at the same time as you're",
      "listing them.");
    return 1;
  }

  // Now let's get down to business! Fetching the thing.
  var fullRecord = getReleaseOrPackageRecord(name);
  var record = fullRecord.record;
  if (!options.list) {

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

    try {
      if (options.add) {
        Console.info("Adding a maintainer to " + name + "...");
        if (fullRecord.release) {
          packageClient.callPackageServer(
            conn, 'addReleaseMaintainer', name, options.add);
        } else {
          packageClient.callPackageServer(
            conn, 'addMaintainer', name, options.add);
        }
      } else if (options.remove) {
        Console.info("Removing a maintainer from " + name + "...");
        if (fullRecord.release) {
          packageClient.callPackageServer(
            conn, 'removeReleaseMaintainer', name, options.remove);
        } else {
          packageClient.callPackageServer(
            conn, 'removeMaintainer', name, options.remove);
        }
        Console.info("Success.");
      }
    } catch (err) {
      packageClient.handlePackageServerConnectionError(err);
      return 1;
    }
    conn.close();

    // Update the catalog so that we have this information, and find the record
    // again so that the message below is correct.
    refreshOfficialCatalogOrDie();
    fullRecord = getReleaseOrPackageRecord(name);
    record = fullRecord.record;
  }

  if (!record) {
    Console.info(
      "Could not get list of maintainers:",
      "package " + name + " does not exist.");
    return 1;
  }

  Console.info();
  Console.info("The maintainers for " + name + " are:");
  _.each(record.maintainers, function (user) {
    if (! user || !user.username)
      Console.rawInfo("<unknown>" + "\n");
    else
      Console.rawInfo(user.username + "\n");
  });
  return 0;
});

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

main.registerCommand({
  name: 'admin make-bootstrap-tarballs',
  minArgs: 2,
  maxArgs: 2,
  hidden: true,

  options: {
    // Copy the tarball contents to the output directory instead of making a
    // tarball (useful for testing the release process)
    "unpacked": { type: Boolean },
    // Build a tarball only for a specific arch
    "target-arch": { type: String }
  },

  // 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.
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  var releaseNameAndVersion = options.args[0];

  // We get this as an argument, so it is an OS path. Make it a standard path.
  var outputDirectory = files.convertToStandardPath(options.args[1]);

  var trackAndVersion = utils.splitReleaseName(releaseNameAndVersion);
  var releaseTrack = trackAndVersion[0];
  var releaseVersion = trackAndVersion[1];

  var releaseRecord = catalog.official.getReleaseVersion(
    releaseTrack, releaseVersion);
  if (!releaseRecord) {
    // XXX this could also mean package unknown.
    Console.error('Release unknown: ' + releaseNameAndVersion + '');
    return 1;
  }

  var toolPackageVersion = releaseRecord.tool &&
        utils.parsePackageAndVersion(releaseRecord.tool);
  if (!toolPackageVersion)
    throw new Error("bad tool in release: " + releaseRecord.tool);
  var toolPackage = toolPackageVersion.package;
  var toolVersion = toolPackageVersion.version;

  var toolPkgBuilds = catalog.official.getAllBuilds(
    toolPackage, toolVersion);
  if (!toolPkgBuilds) {
    // XXX this could also mean package unknown.
    Console.error('Tool version unknown: ' + releaseRecord.tool);
    return 1;
  }
  if (!toolPkgBuilds.length) {
    Console.error('Tool version has no builds: ' + releaseRecord.tool);
    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, 'buildArchitectures');
  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];
  });

  if (options['target-arch']) {
    // check if the passed arch is in the list
    var arch = options['target-arch'];
    if (! _.contains(osArches, arch)) {
      throw new Error(
        arch + ": the arch is not available for the release. Available arches: "
        + osArches.join(', '));
    }

    // build only for the selected arch
    osArches = [arch];
  }

  Console.error(
    'Building bootstrap tarballs for architectures ' + osArches.join(', '));

  // Before downloading anything, check that the catalog contains everything we
  // need for the OSes that the tool is built for.
  main.captureAndExit("=> Errors finding builds:", function () {
    _.each(osArches, function (osArch) {
      _.each(releaseRecord.packages, function (pkgVersion, pkgName) {
        buildmessage.enterJob({
          title: "looking up " + pkgName + "@" + pkgVersion + " on " + osArch
        }, function () {
          if (!catalog.official.getBuildsForArches(pkgName, pkgVersion, [osArch])) {
            buildmessage.error("missing build of " + pkgName + "@" + pkgVersion +
                               " for " + osArch);
          }
        });
      });
    });
  });

  files.mkdir_p(outputDirectory);

  // Get a copy of the data.json.
  var dataTmpdir = files.mkdtemp();
  var tmpDataFile = files.pathJoin(dataTmpdir, 'packages.data.db');

  var tmpCatalog = new catalogRemote.RemoteCatalog();
  tmpCatalog.initialize({
    packageStorage: tmpDataFile
  });
  try {
    packageClient.updateServerPackageData(tmpCatalog, null);
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 2;
  }

  // Since we're making bootstrap tarballs, we intend to recommend this release,
  // so we should ensure that once it is downloaded, it knows it is recommended
  // rather than having a little identity crisis and thinking that a past
  // release is the latest recommended until it manages to sync.
  tmpCatalog.forceRecommendRelease(releaseTrack, releaseVersion);
  tmpCatalog.closePermanently();
  if (files.exists(tmpDataFile + '-wal')) {
    throw Error("Write-ahead log still exists for " + tmpDataFile
                + " so the data file will be incomplete!");
  }

  var packageMap =
        packageMapModule.PackageMap.fromReleaseVersion(releaseRecord);

  _.each(osArches, function (osArch) {
    var tmpdir = files.mkdtemp();
    Console.info("Building tarball for " + osArch);

    // when building for Windows architectures, build Windows-specific
    // tropohouse and bootstrap tarball
    var targetPlatform;
    if (/win/i.test(osArch)) {
      targetPlatform = "win32";
    }

    // We're going to build and tar up a tropohouse in a temporary directory.
    var tmpTropo = new tropohouse.Tropohouse(
      files.pathJoin(tmpdir, '.meteor'),
      { platform: targetPlatform });

    main.captureAndExit(
      "=> Errors downloading packages for " + osArch + ":",
      function () {
        tmpTropo.downloadPackagesMissingFromMap(packageMap, {
          serverArchitectures: [osArch]
        });
      }
    );

    // Install the sqlite DB file we synced earlier. We have previously
    // confirmed that the "-wal" file (which could contain extra log entries
    // that haven't been flushed to the main file yet) doesn't exist, so we
    // don't have to copy it.
    files.copyFile(tmpDataFile, config.getPackageStorage({
      root: tmpTropo.root
    }));

    // Create the top-level 'meteor' symlink, which links to the latest tool's
    // meteor shell script.
    var toolIsopackPath =
          tmpTropo.packagePath(toolPackage, toolVersion);
    var toolIsopack = new isopack.Isopack;
    toolIsopack.initFromPath(toolPackage, toolIsopackPath);
    var toolRecord = _.findWhere(toolIsopack.toolsOnDisk, {arch: osArch});
    if (!toolRecord)
      throw Error("missing tool for " + osArch);

    tmpTropo.linkToLatestMeteor(files.pathJoin(
        tmpTropo.packagePath(toolPackage, toolVersion, true),
        toolRecord.path,
        'meteor'));

    if (options.unpacked) {
      files.cp_r(tmpTropo.root, outputDirectory);
    } else {
      files.createTarball(
        tmpTropo.root,
        files.pathJoin(outputDirectory,
          'meteor-bootstrap-' + osArch + '.tar.gz'));
    }
  });

  return 0;
});

// We will document how to set banners on things in a later release.
main.registerCommand({
  name: 'admin set-banners',
  minArgs: 1,
  maxArgs: 1,
  hidden: true,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {
  var bannersFile = options.args[0];
  try {
    var bannersData = files.readFile(bannersFile, 'utf8');
    bannersData = JSON.parse(bannersData);
  } catch (e) {
    Console.error("Could not parse banners file: " + e.message);
    return 1;
  }
  if (!bannersData.track) {
    Console.error("Banners file should have a 'track' key.");
    return 1;
  }
  if (!bannersData.banners) {
    Console.error("Banners file should have a 'banners' key.");
    return 1;
  }

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

  try {
    packageClient.callPackageServer(
      conn, 'setBannersOnReleases',
      bannersData.track, bannersData.banners);
  } catch (e) {
    packageClient.handlePackageServerConnectionError(e);
    return 1;
  }

  // Refresh afterwards.
  refreshOfficialCatalogOrDie();
  return 0;
});

main.registerCommand({
  name: 'admin recommend-release',
  minArgs: 1,
  maxArgs: 1,
  options: {
    unrecommend: { type: Boolean, short: "u" }
  },
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {

  // We want the most recent information.
  //refreshOfficialCatalogOrDie();
  var release = options.args[0].split('@');
  var name = release[0];
  var version = release[1];
  if (!version) {
    Console.error('Must specify release version (track@version)');
    return 1;
  }

  // Now let's get down to business! Fetching the thing.
  var record = catalog.official.getReleaseTrack(name);
  if (!record) {
    Console.error();
    Console.error('There is no release track named ' + name);
    return 1;
  }

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

  try {
    if (options.unrecommend) {
      Console.info("Unrecommending " + name + "@" + version + "...");
      packageClient.callPackageServer(
        conn, 'unrecommendVersion', name, version);
      Console.info("Success.");
      Console.info(name + "@" + version, "is no longer a recommended release");
    } else {
      Console.info("Recommending " + options.args[0] + "...");
      packageClient.callPackageServer(conn, 'recommendVersion', name, version);
      Console.info("Success.");
      Console.info(name + "@" + version, "is now a recommended release");
    }
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }
  conn.close();
  refreshOfficialCatalogOrDie();

  return 0;
});


main.registerCommand({
  name: 'admin change-homepage',
  minArgs: 2,
  maxArgs: 2,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {

  // We want the most recent information.
  //refreshOfficialCatalogOrDie();
  var name = options.args[0];
  var url = options.args[1];

  // Now let's get down to business! Fetching the thing.
  var record = catalog.official.getPackage(name);
  if (!record) {
    Console.error();
    Console.error('There is no package named ' + name);
    return 1;
  }

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

  try {
    Console.rawInfo(
        "Changing homepage on "
          + name + " to " + url + "...");
      packageClient.callPackageServer(conn,
          '_changePackageHomepage', name, url);
      Console.info(" done");
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }
  conn.close();
  refreshOfficialCatalogOrDie();

  return 0;
});


main.registerCommand({
  name: 'admin set-unmigrated',
  minArgs: 1,
  options: {
    "success" : {type: Boolean}
  },
  hidden: true,
  catalogRefresh: new catalog.Refresh.OnceAtStart({ ignoreErrors: false })
}, function (options) {

  // We don't care about having the most recent information, but we do want the
  // option to either unmigrate a specific version, or to unmigrate an entire
  // package. So, for an entire package, let's get all of its versions.
  var name = options.args[0];
  var versions = [];
  var nSplit = name.split('@');
  if (nSplit.length > 2) {
    throw new main.ShowUsage;
  } else if (nSplit.length == 2) {
    versions = [nSplit[1]];
    name = nSplit[0];
  } else {
    versions = catalog.official.getSortedVersions(name);
  }

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

  try {
    var status = options.success ? "successfully" : "unsuccessfully";
    // XXX: This should probably use progress bars instead.
    _.each(versions, function (version) {
      Console.rawInfo(
        "Setting " + name + "@" + version + " as " +
         status + " migrated ... ");
      packageClient.callPackageServer(
        conn,
        '_changeVersionMigrationStatus',
        name, version, !options.success);
      Console.info("done.");
    });
  } catch (err) {
    packageClient.handlePackageServerConnectionError(err);
    return 1;
  }
  conn.close();
  refreshOfficialCatalogOrDie();

  return 0;
});
