Files
meteor/tools/commands.js
2014-08-11 16:14:14 -07:00

1704 lines
54 KiB
JavaScript

var main = require('./main.js');
var path = require('path');
var _ = require('underscore');
var fs = require('fs');
var files = require('./files.js');
var deploy = require('./deploy.js');
var buildmessage = require('./buildmessage.js');
var project = require('./project.js').project;
var warehouse = require('./warehouse.js');
var auth = require('./auth.js');
var config = require('./config.js');
var release = require('./release.js');
var Future = require('fibers/future');
var runLog = require('./run-log.js');
var packageClient = require('./package-client.js');
var utils = require('./utils.js');
var httpHelpers = require('./http-helpers.js');
var archinfo = require('./archinfo.js');
var tropohouse = require('./tropohouse.js');
var packageCache = require('./package-cache.js');
var packageLoader = require('./package-loader.js');
var PackageSource = require('./package-source.js');
var compiler = require('./compiler.js');
var catalog = require('./catalog.js');
var stats = require('./stats.js');
var unipackage = require('./unipackage.js');
var cordova = require('./commands-cordova.js');
var execFileSync = require('./utils.js').execFileSync;
// The architecture used by Galaxy servers; it's the architecture used
// by 'meteor deploy'.
var DEPLOY_ARCH = 'os.linux.x86_64';
// Given a site name passed on the command line (eg, 'mysite'), return
// a fully-qualified hostname ('mysite.meteor.com').
//
// This is fairly simple for now. It appends 'meteor.com' if the name
// doesn't contain a dot, and it deletes any trailing dots (the
// technically legal hostname 'mysite.com.' is canonicalized to
// 'mysite.com').
//
// In the future, you should be able to make this default to some
// other domain you control, rather than 'meteor.com'.
var qualifySitename = function (site) {
if (site.indexOf(".") === -1)
site = site + ".meteor.com";
while (site.length && site[site.length - 1] === ".")
site = site.substring(0, site.length - 1);
return site;
};
// Given a (non necessarily fully qualified) site name from the
// command line, return true if the site is hosted by a Galaxy, else
// false.
var hostedWithGalaxy = function (site) {
var site = qualifySitename(site);
return !! require('./deploy-galaxy.js').discoverGalaxy(site);
};
// Get all local packages available. Returns a map from the package name to the
// version record for that package.
var getLocalPackages = function () {
var ret = {};
var names = catalog.complete.getAllPackageNames();
_.each(names, function (name) {
if (catalog.complete.isLocalPackage(name)) {
ret[name] = catalog.complete.getLatestVersion(name);
}
});
return ret;
};
var parseHostPort = function (str) {
// XXX factor this out into a {type: host/port}?
var portMatch = str.match(/^(?:(.+):)?([0-9]+)$/);
if (!portMatch) {
throw new Error(
"run: --port (-p) must be a number or be of the form 'host:port' where\n" +
"port is a number. Try 'meteor help run' for help.\n");
}
var host = portMatch[1] || 'localhost';
var port = parseInt(portMatch[2]);
return {
host: host,
port: port
};
};
///////////////////////////////////////////////////////////////////////////////
// options that act like commands
///////////////////////////////////////////////////////////////////////////////
// Prints the Meteor architecture name of this host
main.registerCommand({
name: '--arch',
requiresRelease: false
}, function (options) {
var archinfo = require('./archinfo.js');
console.log(archinfo.host());
});
// Prints the current release in use. Note that if there is not
// actually a specific release, we print to stderr and exit non-zero,
// while if there is a release we print to stdout and exit zero
// (making this useful to scripts).
// XXX: What does this mean in our new release-free world?
main.registerCommand({
name: '--version',
requiresRelease: false
}, function (options) {
if (release.current === null) {
if (! options.appDir)
throw new Error("missing release, but not in an app?");
process.stderr.write(
"This project was created with a checkout of Meteor, rather than an\n" +
"official release, and doesn't have a release number associated with\n" +
"it. You can set its release with 'meteor update'.\n");
return 1;
}
if (release.current.isCheckout()) {
process.stderr.write("Unreleased (running from a checkout)\n");
return 1;
}
console.log(release.current.getDisplayName());
});
// Internal use only. For automated testing.
main.registerCommand({
name: '--long-version',
requiresRelease: false
}, function (options) {
if (files.inCheckout()) {
process.stderr.write("checkout\n");
return 1;
} else if (release.current === null) {
// .meteor/release says "none" but not in a checkout.
process.stderr.write("none\n");
return 1;
} else {
process.stdout.write(release.current.name + "\n");
process.stdout.write(files.getToolsVersion() + "\n");
return 0;
}
});
// Internal use only. For automated testing.
main.registerCommand({
name: '--requires-release',
requiresRelease: true
}, function (options) {
return 0;
});
// Internal use only. Makes sure that your Meteor install is totally good to go
// (is "airplane safe"). Specifically, it:
// - Builds all local packages (including their npm dependencies)
// - Ensures that all packages in your current release are downloaded
// - 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). If you want to rebuild all local packages,
// 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'
}, function (options) {
// It is not strictly needed, but it is thematically a good idea to refresh
// the official catalog when we call get-ready, since it is an
// internet-requiring action.
catalog.official.refresh();
var loadPackages = function (packagesToLoad, loader) {
buildmessage.assertInCapture();
_.each(packagesToLoad, function (name) {
// Calling getPackage on the loader will return a unipackage object, which
// means that the package will be compiled/downloaded. That we throw the
// package variable away afterwards is immaterial.
loader.getPackage(name);
});
};
var messages = buildmessage.capture({
title: 'getting packages ready'
}, function () {
// First, build all accessible *local* packages, whether or not this app
// uses them. Use the "all packages are local" loader.
loadPackages(catalog.complete.getLocalPackageNames(),
new packageLoader.PackageLoader({versions: null}));
// In an app? Get the list of packages used by this app. Calling getVersions
// on the project will ensureDepsUpToDate which will ensure that all builds
// of everything we need from versions have been downloaded. (Calling
// buildPackages may be redundant, but can't hurt.)
if (options.appDir) {
loadPackages(_.keys(project.getVersions()), project.getPackageLoader());
}
// Using a release? Get all the packages in the release.
if (release.current.isProperRelease()) {
var releasePackages = release.current.getPackages();
// HACK: relies on fact that the function below doesn't actually
// have any relation to the project directory
project._ensurePackagesExistOnDisk(releasePackages, { verbose: true });
loadPackages(
_.keys(releasePackages),
new packageLoader.PackageLoader({versions: releasePackages}));
}
});
if (messages.hasMessages()) {
process.stderr.write("\n" + messages.formatMessages());
return 1;
};
console.log("You are ready!");
return 0;
});
///////////////////////////////////////////////////////////////////////////////
// run
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'run',
requiresApp: true,
maxArgs: Infinity,
options: {
port: { type: String, short: "p", default: '3000' },
'app-port': { type: String },
production: { type: Boolean },
'raw-logs': { type: Boolean },
settings: { type: String },
'no-server': { type: Boolean },
program: { type: String },
// With --once, meteor does not re-run the project if it crashes
// and does not monitor for file changes. Intentionally
// undocumented: intended for automated testing (eg, cli-test.sh),
// not end-user use. #Once
once: { type: Boolean }
}
}, function (options) {
// Calculate project versions. (XXX: Theoretically, we should not be doing it
// here. We should do it lazily, if the command calls for it. However, we do
// end up recalculating them for stats, for example, and, more importantly, we
// have noticed some issues if we just leave this in the air. We think that
// those issues are concurrency-related, or possibly some weird
// order-of-execution of interaction that we are failing to understand. This
// seems to fix it in a clear and understandable fashion.)
var messages = buildmessage.capture(function () {
project.getVersions(); // #StructuredProjectInitialization
});
if (messages.hasMessages()) {
process.stderr.write(messages.formatMessages());
return 1;
}
try {
var parsedHostPort = parseHostPort(options.port);
} catch (err) {
process.stderr.write(err.message);
return 1;
}
// Always bundle for the browser by default.
var webArchs = ["web.browser"];
// If additional args were specified, then also start a mobile build.
if (options.args.length) {
webArchs.push("web.cordova");
// will asynchronously start mobile emulators/devices
try {
var appName = path.basename(options.appDir);
var localPath = path.join(options.appDir, '.meteor', 'local');
cordova.buildPlatforms(localPath, options.args,
_.extend({ appName: appName }, options, parsedHostPort));
cordova.runPlatforms(localPath, options.args);
} catch (err) {
process.stderr.write(err.message + '\n');
return 1;
}
}
if (options['no-server'])
return 0;
var appHost, appPort;
if (options['app-port']) {
var appPortMatch = options['app-port'].match(/^(?:(.+):)?([0-9]+)?$/);
if (!appPortMatch) {
process.stderr.write(
"run: --app-port must be a number or be of the form 'host:port' where\n" +
"port is a number. Try 'meteor help run' for help.\n");
return 1;
}
appHost = appPortMatch[1] || null;
// It's legit to specify `--app-port host:` and still let the port be
// randomized.
appPort = appPortMatch[2] ? parseInt(appPortMatch[2]) : null;
}
if (release.forced) {
var appRelease = project.getMeteorReleaseVersion();
if (release.current.name !== appRelease) {
console.log("=> Using Meteor %s as requested (overriding Meteor %s)",
release.current.name, appRelease);
console.log();
}
}
auth.tryRevokeOldTokens({timeout: 1000});
if (options['raw-logs'])
runLog.setRawLogs(true);
var runAll = require('./run-all.js');
return runAll.run(options.appDir, {
proxyPort: parsedHostPort.port,
proxyHost: parsedHostPort.host,
appPort: appPort,
appHost: appHost,
settingsFile: options.settings,
program: options.program || undefined,
buildOptions: {
minify: options.production,
webArchs: webArchs
},
rootUrl: process.env.ROOT_URL,
mongoUrl: process.env.MONGO_URL,
oplogUrl: process.env.MONGO_OPLOG_URL,
once: options.once
});
});
///////////////////////////////////////////////////////////////////////////////
// create
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'create',
maxArgs: 1,
options: {
list: { type: Boolean },
example: { type: String },
package: { type: Boolean }
}
}, function (options) {
// Creating a package is much easier than creating an app, so if that's what
// we are doing, do that first. (For example, we don't springboard to the
// latest release to create a package if we are inside an app)
if (options.package) {
var packageName = options.args[0];
// No package examples exist yet.
if (options.list && options.example) {
process.stderr.write("No package examples exist at this time.\n\n");
throw new main.ShowUsage;
}
if (fs.existsSync(packageName)) {
process.stderr.write(packageName + ": Already exists\n");
return 1;
}
var transform = function (x) {
var xn = x.replace(/~name~/g, packageName);
// If we are running from checkout, comment out the line sourcing packages
// from a release, with the latest release filled in (in case they do want
// to publish later). If we are NOT running from checkout, fill it out
// with the current release.
var relString;
if (release.current.isCheckout()) {
xn = xn.replace(/~cc~/g, "//");
var rel = catalog.complete.getDefaultReleaseVersion();
var relString = rel.track + "@" + rel.version;
} else {
xn = xn.replace(/~cc~/g, "");
relString = release.current.name;
}
// If we are not in checkout, write the current release here.
return xn.replace(/~release~/g, relString);
};
files.cp_r(path.join(__dirname, 'skel-pack'), packageName, {
transformFilename: function (f) {
return transform(f);
},
transformContents: function (contents, f) {
if ((/(\.html|\.js|\.css)/).test(f))
return new Buffer(transform(contents.toString()));
else
return contents;
},
ignore: [/^local$/]
});
process.stdout.write(packageName + ": created\n");
return 0;
}
// Suppose you have an app A, and from some directory inside that
// app, you run 'meteor create /my/new/app'. The new app should use
// the latest available Meteor release, not the release that A
// uses. So if we were run from inside an app directory, and the
// user didn't force a release with --release, we need to
// springboard to the correct release and tools version.
//
// (In particular, it's not sufficient to create the new app with
// this version of the tools, and then stamp on the correct release
// at the end.)
if (! release.current.isCheckout() &&
release.current.name !== release.latestDownloaded() &&
! release.forced) {
throw new main.SpringboardToLatestRelease;
}
var exampleDir = path.join(__dirname, '..', 'examples');
var examples = _.reject(fs.readdirSync(exampleDir), function (e) {
return (e === 'unfinished' || e === 'other' || e[0] === '.');
});
if (options.list) {
process.stdout.write("Available examples:\n");
_.each(examples, function (e) {
process.stdout.write(" " + e + "\n");
});
process.stdout.write("\n" +
"Create a project from an example with 'meteor create --example <name>'.\n");
return 0;
};
var appPath;
if (options.args.length === 1)
appPath = options.args[0];
else if (options.example)
appPath = options.example;
else
throw new main.ShowUsage;
if (fs.existsSync(appPath)) {
process.stderr.write(appPath + ": Already exists\n");
return 1;
}
if (files.findAppDir(appPath)) {
process.stderr.write(
"You can't create a Meteor project inside another Meteor project.\n");
return 1;
}
var transform = function (x) {
return x.replace(/~name~/g, path.basename(appPath));
};
if (options.example) {
if (examples.indexOf(options.example) === -1) {
process.stderr.write(options.example + ": no such example\n\n");
process.stderr.write("List available applications with 'meteor create --list'.\n");
return 1;
} else {
files.cp_r(path.join(exampleDir, options.example), appPath, {
ignore: [/^local$/]
});
}
} else {
files.cp_r(path.join(__dirname, 'skel'), appPath, {
transformFilename: function (f) {
return transform(f);
},
transformContents: function (contents, f) {
if ((/(\.html|\.js|\.css)/).test(f))
return new Buffer(transform(contents.toString()));
else
return contents;
},
ignore: [/^local$/]
});
}
// We are actually working with a new meteor project at this point, so
// reorient its path. We might do some things to it, but they should be
// invisible to the user, so mute non-error output.
project.setRootDir(appPath);
project.setMuted(true);
project.writeMeteorReleaseVersion(
release.current.isCheckout() ? "none" : release.current.name);
var messages = buildmessage.capture(function () {
project._ensureDepsUpToDate();
});
if (messages.hasMessages()) {
process.stderr.write(messages.formatMessages());
return 1;
}
process.stdout.write(appPath + ": created");
if (options.example && options.example !== appPath)
process.stderr.write(" (from '" + options.example + "' template)");
process.stdout.write(".\n\n");
process.stdout.write(
"To run your new app:\n" +
" cd " + appPath + "\n" +
" meteor\n");
});
///////////////////////////////////////////////////////////////////////////////
// run-upgrader
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'run-upgrader',
hidden: true,
minArgs: 1,
maxArgs: 1,
requiresApp: true
}, function (options) {
var upgrader = options.args[0];
var upgraders = require("./upgraders.js");
console.log("%s: running upgrader %s.",
path.basename(options.appDir), upgrader);
upgraders.runUpgrader(upgrader);
});
///////////////////////////////////////////////////////////////////////////////
// bundle
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'bundle',
minArgs: 1,
maxArgs: 1,
requiresApp: true,
options: {
debug: { type: Boolean },
directory: { type: Boolean },
architecture: { type: String },
// Undocumented
'for-deploy': { type: Boolean },
port: { type: String, short: "p", default: "localhost:3000" },
settings: { type: String}, // XXX document
'ios-path': { type: String },
'android-path': { type: String },
}
}, function (options) {
// XXX if they pass a file that doesn't end in .tar.gz or .tgz, add
// the former for them
// XXX output, to stderr, the name of the file written to (for human
// comfort, especially since we might change the name)
// XXX name the root directory in the bundle based on the basename
// of the file, not a constant 'bundle' (a bit obnoxious for
// machines, but worth it for humans)
// Error handling for options.architecture. We must pass in only one of three
// architectures. See archinfo.js for more information on what the
// architectures are, what they mean, et cetera.
var VALID_ARCHITECTURES =
["os.osx.x86_64", "os.linux.x86_64", "os.linux.x86_32"];
if (options.architecture &&
_.indexOf(VALID_ARCHITECTURES, options.architecture) === -1) {
process.stderr.write("Invalid architecture: " + options.architecture + "\n");
process.stderr.write(
"Please use one of the following: " + VALID_ARCHITECTURES + "\n");
process.exit(1);
}
var bundleArch = options.architecture || archinfo.host();
var localPath = path.join(options.appDir, '.meteor', 'local');
var mobilePlatforms = {};
if (options['ios-path'])
mobilePlatforms.ios = options['ios-path'];
if (options['android-path'])
mobilePlatforms.android = options['android-path'];
if (! _.isEmpty(mobilePlatforms)) {
var cordovaSettings = {};
try {
var parsedHostPort = parseHostPort(options.port);
} catch (err) {
process.stderr.write(err.message);
return 1;
}
cordova.buildPlatforms(localPath, _.keys(mobilePlatforms),
_.extend({}, options, parsedHostPort, {
appName: path.basename(options.appDir)
}));
}
var buildDir = path.join(localPath, 'build_tar');
var outputPath = path.resolve(options.args[0]); // get absolute path
var bundlePath = options['directory'] ?
outputPath : path.join(buildDir, 'bundle');
var loader;
var messages = buildmessage.capture(function () {
loader = project.getPackageLoader();
});
if (messages.hasMessages()) {
process.stderr.write("Errors prevented bundling your app:\n");
process.stderr.write(messages.formatMessages());
return 1;
}
var bundler = require(path.join(__dirname, 'bundler.js'));
var bundleResult = bundler.bundle({
outputPath: bundlePath,
buildOptions: {
minify: ! options.debug,
// XXX is this a good idea, or should linux be the default since
// that's where most people are deploying
// default? i guess the problem with using DEPLOY_ARCH as default
// is then 'meteor bundle' with no args fails if you have any local
// packages with binary npm dependencies
arch: bundleArch
}
});
if (bundleResult.errors) {
process.stderr.write("Errors prevented bundling:\n");
process.stderr.write(bundleResult.errors.formatMessages());
return 1;
}
// Copy over the Cordova builds AFTER we bundle so that they are not included
// in the main bundle.
_.each(mobilePlatforms, function (platformPath, platformName) {
var buildPath = path.join(localPath, 'cordova-build',
'platforms', platformName);
files.cp_r(buildPath, platformPath);
});
if (!options['directory']) {
try {
files.createTarball(path.join(buildDir, 'bundle'), outputPath);
} catch (err) {
console.log(JSON.stringify(err));
process.stderr.write("Couldn't create tarball\n");
}
}
files.rm_recursive(buildDir);
});
///////////////////////////////////////////////////////////////////////////////
// mongo
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'mongo',
maxArgs: 1,
options: {
url: { type: Boolean, short: 'U' }
},
requiresApp: function (options) {
return options.args.length === 0;
}
}, function (options) {
var mongoUrl;
var usedMeteorAccount = false;
if (options.args.length === 0) {
// localhost mode
var findMongoPort =
require('./run-mongo.js').findMongoPort;
var mongoPort = findMongoPort(options.appDir);
// XXX detect the case where Meteor is running, but MONGO_URL was
// specified?
if (! mongoPort) {
process.stdout.write(
"mongo: Meteor isn't running a local MongoDB server.\n" +
"\n" +
"This command only works while Meteor is running your application\n" +
"locally. Start your application first. (This error will also occur if\n" +
"you asked Meteor to use a different MongoDB server with $MONGO_URL when\n" +
"you ran your application.)\n" +
"\n" +
"If you're trying to connect to the database of an app you deployed\n" +
"with 'meteor deploy', specify your site's name with this command.\n"
);
return 1;
}
mongoUrl = "mongodb://127.0.0.1:" + mongoPort + "/meteor";
} else {
// remote mode
var site = qualifySitename(options.args[0]);
config.printUniverseBanner();
if (hostedWithGalaxy(site)) {
var deployGalaxy = require('./deploy-galaxy.js');
mongoUrl = deployGalaxy.temporaryMongoUrl(site);
} else {
mongoUrl = deploy.temporaryMongoUrl(site);
usedMeteorAccount = true;
}
if (! mongoUrl)
// temporaryMongoUrl() will have printed an error message
return 1;
}
if (options.url) {
console.log(mongoUrl);
} else {
if (usedMeteorAccount)
auth.maybePrintRegistrationLink();
process.stdin.pause();
var runMongo = require('./run-mongo.js');
runMongo.runMongoShell(mongoUrl);
throw new main.WaitForExit;
}
});
///////////////////////////////////////////////////////////////////////////////
// reset
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'reset',
// Doesn't actually take an argument, but we want to print an custom
// error message if they try to pass one.
maxArgs: 1,
requiresApp: true
}, function (options) {
if (options.args.length !== 0) {
process.stderr.write(
"meteor reset only affects the locally stored database.\n" +
"\n" +
"To reset a deployed application use\n" +
" meteor deploy --delete appname\n" +
"followed by\n" +
" meteor deploy appname\n");
return 1;
}
// XXX detect the case where Meteor is running the app, but
// MONGO_URL was set, so we don't see a Mongo process
var findMongoPort =
require(path.join(__dirname, 'run-mongo.js')).findMongoPort;
var isRunning = !! findMongoPort(options.appDir);
if (isRunning) {
process.stderr.write(
"reset: Meteor is running.\n" +
"\n" +
"This command does not work while Meteor is running your application.\n" +
"Exit the running Meteor development server.\n");
return 1;
}
var localDir = path.join(options.appDir, '.meteor', 'local');
files.rm_recursive(localDir);
process.stdout.write("Project reset.\n");
});
///////////////////////////////////////////////////////////////////////////////
// deploy
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'deploy',
minArgs: 1,
maxArgs: 1,
options: {
'delete': { type: Boolean, short: 'D' },
debug: { type: Boolean },
settings: { type: String },
star: { type: String },
// No longer supported, but we still parse it out so that we can
// print a custom error message.
password: { type: String },
// Shouldn't be documented until the Galaxy release. Marks the
// application as an admin app, so that it will be available in
// Galaxy admin interface.
admin: { type: Boolean },
// Override architecture to deploy whatever stuff we have locally, even if
// it contains binary packages that should be incompatible. A hack to allow
// people to deploy from checkout or do other weird shit. We are not
// responsible for the consequences.
'override-architecture-with-local' : { type: Boolean }
},
requiresApp: function (options) {
return options.delete || options.star ? false : true;
}
}, function (options) {
var site = qualifySitename(options.args[0]);
config.printUniverseBanner();
var useGalaxy = hostedWithGalaxy(site);
var deployGalaxy;
if (options.delete) {
if (useGalaxy) {
deployGalaxy = require('./deploy-galaxy.js');
return deployGalaxy.deleteApp(site);
} else {
return deploy.deleteApp(site);
}
}
// We are actually going to end up compiling an app at this point, so figure
// out its versions. . (XXX: Theoretically, we should not be doing it here. We
// should do it lazily, if the command calls for it. However, we do end up
// recalculating them for stats, for example, and, more importantly, we have
// noticed some issues if we just leave this in the air. We think that those
// issues are concurrency-related, or possibly some weird order-of-execution
// of interaction that we are failing to understand. This seems to fix it in a
// clear and understandable fashion.)
var messages = buildmessage.capture(function () {
project.getVersions(); // #StructuredProjectInitialization
});
if (messages.hasMessages()) {
process.stderr.write(messages.formatMessages());
return 1;
}
if (options.password) {
if (useGalaxy) {
process.stderr.write("Galaxy does not support --password.\n");
} else {
process.stderr.write(
"Setting passwords on apps is no longer supported. Now there are\n" +
"user accounts and your apps are associated with your account so that\n" +
"only you (and people you designate) can access them. See the\n" +
"'meteor claim' and 'meteor authorized' commands.\n");
}
return 1;
}
var starball = options.star;
if (starball && ! useGalaxy) {
// XXX it would be nice to support this for non-Galaxy deploys too
process.stderr.write(
"--star: only supported when deploying to Galaxy.\n");
return 1;
}
var loggedIn = auth.isLoggedIn();
if (! loggedIn) {
process.stderr.write(
"To instantly deploy your app on a free testing server, just enter your\n" +
"email address!\n" +
"\n");
if (! auth.registerOrLogIn())
return 1;
}
// Override architecture iff applicable.
var buildArch = DEPLOY_ARCH;
if (options['override-architecture-with-local']) {
process.stdout.write(
"\n => WARNING: OVERRIDING DEPLOY ARCHITECTURE WITH LOCAL ARCHITECTURE\n");
process.stdout.write(
" => If your app contains binary code, it may break terribly and you will be sad.\n\n");
buildArch = archinfo.host();
}
var buildOptions = {
minify: ! options.debug,
arch: buildArch
};
var deployResult;
if (useGalaxy) {
deployGalaxy = require('./deploy-galaxy.js');
deployResult = deployGalaxy.deploy({
app: site,
appDir: options.appDir,
settingsFile: options.settings,
starball: starball,
buildOptions: buildOptions,
admin: options.admin
});
} else {
deployResult = deploy.bundleAndDeploy({
appDir: options.appDir,
site: site,
settingsFile: options.settings,
buildOptions: buildOptions
});
}
if (deployResult === 0) {
auth.maybePrintRegistrationLink({
leadingNewline: true,
// If the user was already logged in at the beginning of the
// deploy, then they've already been prompted to set a password
// at least once before, so we use a slightly different message.
firstTime: ! loggedIn
});
}
return deployResult;
});
///////////////////////////////////////////////////////////////////////////////
// logs
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'logs',
minArgs: 1,
maxArgs: 1,
options: {
// XXX once Galaxy is released, document this
stream: { type: Boolean, short: 'f' }
}
}, function (options) {
var site = qualifySitename(options.args[0]);
if (hostedWithGalaxy(site)) {
var deployGalaxy = require('./deploy-galaxy.js');
var ret = deployGalaxy.logs({
app: site,
streaming: options.stream
});
if (options.stream && ret === null)
throw new main.WaitForExit;
return ret;
} else {
return deploy.logs(site);
}
});
///////////////////////////////////////////////////////////////////////////////
// open-ide
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'open-ide',
requiresApp: true,
minArgs: 1,
maxArgs: Infinity,
options: {
settings: { type: String },
port: { type: String, short: "p", default: 'localhost:3000' },
production: { type: Boolean }
}
}, function (options) {
// XXX replace try-catch with buildmessage.capture
try {
var localPath = path.join(options.appDir, '.meteor', 'local');
var platforms = options.args;
// check that every passed argument is in fact a platform we can build for
_.each(platforms, cordova.checkIsValidPlatform);
var parsedHostPort = parseHostPort(options.port);
// open projects in ides
cordova.preparePlatforms(localPath, platforms,
_.extend({}, options, parsedHostPort));
_.each(platforms, function (platform) {
if (platform !== 'ios')
throw new Error(platform + ": unsupported platform for 'open-ide' command. Only 'ios' is supportted at the moment.");
execFileSync('sh', ['-c', 'open ' + path.join(localPath, 'cordova-build', 'platforms', 'ios', '*.xcodeproj')]);
});
} catch (err) {
process.stderr.write(err.message + '\n');
return 1;
}
});
///////////////////////////////////////////////////////////////////////////////
// authorized
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'authorized',
minArgs: 1,
maxArgs: 1,
options: {
add: { type: String, short: "a" },
remove: { type: String, short: "r" },
list: { type: Boolean }
}
}, function (options) {
if (options.add && options.remove) {
process.stderr.write(
"Sorry, you can only add or remove one user at a time.\n");
return 1;
}
if ((options.add || options.remove) && options.list) {
process.stderr.write(
"Sorry, you can't change the users at the same time as you're listing them.\n");
return 1;
}
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (hostedWithGalaxy(site)) {
process.stderr.write(
"Sites hosted on Galaxy do not have an authorized user list.\n" +
"Instead, go to your Galaxy dashboard to change the authorized users\n" +
"of your Galaxy.\n");
return 1;
}
if (! auth.isLoggedIn()) {
process.stderr.write(
"You must be logged in for that. Try 'meteor login'.\n");
return 1;
}
if (options.add)
return deploy.changeAuthorized(site, "add", options.add);
else if (options.remove)
return deploy.changeAuthorized(site, "remove", options.remove);
else
return deploy.listAuthorized(site);
});
///////////////////////////////////////////////////////////////////////////////
// claim
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'claim',
minArgs: 1,
maxArgs: 1
}, function (options) {
config.printUniverseBanner();
auth.pollForRegistrationCompletion();
var site = qualifySitename(options.args[0]);
if (! auth.isLoggedIn()) {
process.stderr.write(
"You must be logged in to claim sites. Use 'meteor login' to log in.\n" +
"If you don't have a Meteor developer account yet, create one by clicking\n" +
"'Sign in' and then 'Create account' at www.meteor.com.\n\n");
return 1;
}
if (hostedWithGalaxy(site)) {
process.stderr.write(
"Sorry, you can't claim sites that are hosted on Galaxy.\n");
return 1;
}
return deploy.claim(site);
});
///////////////////////////////////////////////////////////////////////////////
// test-packages
///////////////////////////////////////////////////////////////////////////////
//
// Test your local packages.
//
main.registerCommand({
name: 'test-packages',
maxArgs: Infinity,
options: {
port: { type: String, short: "p", default: "localhost:3000" },
deploy: { type: String },
production: { type: Boolean },
settings: { type: String },
// Undocumented. See #Once
once: { type: Boolean },
// Undocumented. To ensure that QA covers both
// PollingObserveDriver and OplogObserveDriver, this option
// disables oplog for tests. (It still creates a replset, it just
// doesn't do oplog tailing.)
'disable-oplog': { type: Boolean },
// Undocumented flag to use a different test driver.
'driver-package': { type: String },
// Undocumented, sets the path of where the temp app should be created
'test-app-path': { type: String },
// hard-coded options with all known Cordova platforms
ios: { type: Boolean },
'ios-device': { type: Boolean },
android: { type: Boolean },
'android-device': { type: Boolean }
}
}, function (options) {
try {
var parsedHostPort = parseHostPort(options.port);
} catch (err) {
process.stderr.write(err.message);
return 1;
}
// XXX not good to change the options this way
_.extend(options, parsedHostPort);
var testPackages = null;
try {
var packages = getPackagesForTest(options.args);
testPackages = packages.testPackages;
localPackages = packages.localPackages;
options.localPackageNames = packages.localPackages;
} catch (err) {
process.stderr.write('\n' + err.message);
return 1;
}
// Make a temporary app dir (based on the test runner app). This will be
// cleaned up on process exit. Using a temporary app dir means that we can
// run multiple "test-packages" commands in parallel without them stomping
// on each other.
//
// Note: testRunnerAppDir deliberately DOES NOT MATCH the app
// package search path baked into release.current.catalog: we are
// bundling the test runner app, but finding app packages from the
// current app (if any).
var testRunnerAppDir =
options['test-app-path'] || files.mkdtemp('meteor-test-run');
files.cp_r(path.join(__dirname, 'test-runner-app'), testRunnerAppDir);
// We are going to operate in the special test project, so let's remap our
// main project to the test directory.
project.setRootDir(testRunnerAppDir);
project.setMuted(true); // Mute output where applicable
project.writeMeteorReleaseVersion(release.current.name || 'none');
project.forceEditPackages(
[options['driver-package'] || 'test-in-browser'],
'add');
var mobileOptions = ['ios', 'ios-device', 'android', 'android-device'];
var mobilePlatforms = [];
_.each(mobileOptions, function (option) {
if (options[option])
mobilePlatforms.push(option);
});
if (! _.isEmpty(mobilePlatforms)) {
var localPath = path.join(testRunnerAppDir, '.meteor', 'local');
var platforms =
_.map(mobilePlatforms, function (t) { return t.replace(/-device$/, ''); });
platforms = _.uniq(platforms);
project.addCordovaPlatforms(platforms);
try {
cordova.buildPlatforms(localPath, mobilePlatforms,
_.extend({}, options, {
appName: path.basename(testRunnerAppDir)
}));
cordova.runPlatforms(localPath, mobilePlatforms);
} catch (err) {
process.stderr.write(err.message + '\n');
return 1;
}
}
return runTestAppForPackages(testPackages, testRunnerAppDir, options);
});
// Ensures that packages are prepared and built for testing
var getPackagesForTest = function (packages) {
var testPackages;
var localPackageNames = [];
if (packages.length === 0) {
// Only test local packages if no package is specified.
// XXX should this use the new getLocalPackageNames?
var packageList = getLocalPackages();
if (! packageList) {
// Couldn't load the package list, probably because some package
// has a parse error. Bail out -- this kind of sucks; we would
// like to find a way to get reloading.
throw new Error("No packages to test");
}
testPackages = _.keys(packageList);
} else {
var messages = buildmessage.capture(function () {
testPackages = _.map(packages, function (p) {
return buildmessage.enterJob({
title: "trying to test package `" + p + "`"
}, function () {
// If it's a package name, just pass it through.
if (p.indexOf('/') === -1) {
if (p.indexOf('@') !== -1) {
buildmessage.error(
"You may not specify versions for local packages: " + p );
// Recover by returning p anyway.
}
// Check to see if this is a real package, and if it is a real
// package, if it has tests.
var versionRec = catalog.complete.getLatestVersion(p);
if (!versionRec) {
buildmessage.error(
"Unknown package: " + p );
}
if (!catalog.complete.isLocalPackage(p)) {
buildmessage.error(
"Not a local package, cannot test: " + p );
return p;
}
if (versionRec && !versionRec.testName) {
buildmessage.error(
"There are no tests for package: " + p );
}
return p;
}
// Otherwise it's a directory; load it into a Package now. Use
// path.resolve to strip trailing slashes, so that packageName doesn't
// have a trailing slash.
//
// Why use addLocalPackage instead of just loading the packages
// and passing Unipackage objects to the bundler? Because we
// actually need the Catalog to know about the package, so that
// we are able to resolve the test package's dependency on the
// main package. This is not ideal (I hate how this mutates global
// state) but it'll do for now.
var packageDir = path.resolve(p);
var packageName = path.basename(packageDir);
catalog.complete.addLocalPackage(packageName, packageDir);
localPackageNames.push(packageName);
return packageName;
});
});
});
if (messages.hasMessages()) {
process.stderr.write("\n" + messages.formatMessages());
return 1;
}
}
return { testPackages: testPackages, localPackages: localPackageNames };
};
var runTestAppForPackages = function (testPackages, testRunnerAppDir, options) {
// When we test packages, we need to know their versions and all of their
// dependencies. We are going to add them to the project and have the project
// compute them for us. This means that right now, we are testing all packages
// as they work together.
var tests = [];
_.each(testPackages, function(name) {
var versionRecord = catalog.complete.getLatestVersion(name);
if (versionRecord && versionRecord.testName) {
tests.push(versionRecord.testName);
}
});
project.forceEditPackages(tests, 'add');
var buildOptions = {
minify: options.production
};
var ret;
if (options.deploy) {
buildOptions.arch = DEPLOY_ARCH;
ret = deploy.bundleAndDeploy({
appDir: testRunnerAppDir,
site: options.deploy,
settingsFile: options.settings,
buildOptions: buildOptions,
recordPackageUsage: false
});
} else {
var runAll = require('./run-all.js');
ret = runAll.run(testRunnerAppDir, {
// if we're testing packages from an app, we still want to make
// sure the user doesn't 'meteor update' in the app, requiring
// a switch to a different release
appDirForVersionCheck: options.appDir,
proxyPort: options.port,
disableOplog: options['disable-oplog'],
settingsFile: options.settings,
banner: "Tests",
buildOptions: buildOptions,
rootUrl: process.env.ROOT_URL,
mongoUrl: process.env.MONGO_URL,
oplogUrl: process.env.MONGO_OPLOG_URL,
once: options.once,
recordPackageUsage: false
});
}
_.each(options.localPackageNames || [], function (name) {
catalog.complete.removeLocalPackage(name);
});
return ret;
};
///////////////////////////////////////////////////////////////////////////////
// rebuild
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'rebuild',
maxArgs: Infinity,
hidden: true
}, function (options) {
var messages;
var count = 0;
// No packages specified. Rebuild everything.
if (options.args.length === 0) {
if (options.appDir) {
// The catalog doesn't know about other programs in your app. Let's blow
// away their .build directories if they have them, and not rebuild
// them. Sort of hacky, but eh.
var programsDir = project.getProgramsDirectory();
var programsSubdirs = project.getProgramsSubdirs();
_.each(programsSubdirs, function (program) {
// The implementation of this part of the function might change once we
// change the control file format to explicitly specify packages and
// programs instead of just loading everything in the programs directory?
files.rm_recursive(path.join(programsDir, program, '.build.' + program));
});
}
messages = buildmessage.capture(function () {
count = catalog.complete.rebuildLocalPackages();
});
} else {
messages = buildmessage.capture(function () {
count = catalog.complete.rebuildLocalPackages(options.args);
});
}
if (count)
console.log("Built " + count + " packages.");
if (messages.hasMessages()) {
process.stderr.write("\n" + messages.formatMessages());
return 1;
}
});
///////////////////////////////////////////////////////////////////////////////
// login
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'login',
options: {
email: { type: String },
// Undocumented: get credentials on a specific Galaxy. Do we still
// need this?
galaxy: { type: String }
}
}, function (options) {
return auth.loginCommand(_.extend({
overwriteExistingToken: true
}, options));
});
///////////////////////////////////////////////////////////////////////////////
// logout
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'logout'
}, function (options) {
return auth.logoutCommand(options);
});
///////////////////////////////////////////////////////////////////////////////
// whoami
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'whoami'
}, function (options) {
return auth.whoAmICommand(options);
});
///////////////////////////////////////////////////////////////////////////////
// admin make-bootstrap-tarballs
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'admin make-bootstrap-tarballs',
minArgs: 2,
maxArgs: 2,
hidden: true,
}, function (options) {
var releaseNameAndVersion = options.args[0];
var outputDirectory = options.args[1];
// In this function, we want to use the official catalog everywhere, because
// we assume that all packages have been published (along with the release
// obviously) and we want to be sure to only bundle the published versions.
catalog.official.refresh();
var parsed = utils.splitConstraint(releaseNameAndVersion);
if (!parsed.constraint)
throw new main.ShowUsage;
var release = catalog.official.getReleaseVersion(parsed.package,
parsed.constraint);
if (!release) {
// XXX this could also mean package unknown.
process.stderr.write('Release unknown: ' + releaseNameAndVersion + '\n');
return 1;
}
var toolPkg = release.tool && utils.splitConstraint(release.tool);
if (! (toolPkg && toolPkg.constraint))
throw new Error("bad tool in release: " + toolPkg);
var toolPkgBuilds = catalog.official.getAllBuilds(
toolPkg.package, toolPkg.constraint);
if (!toolPkgBuilds) {
// XXX this could also mean package unknown.
process.stderr.write('Tool version unknown: ' + release.tool + '\n');
return 1;
}
if (!toolPkgBuilds.length) {
process.stderr.write('Tool version has no builds: ' + release.tool + '\n');
return 1;
}
// XXX check to make sure this is the three arches that we want? it's easier
// during 0.9.0 development to allow it to just decide "ok, i just want to
// build the OSX tarball" though.
var buildArches = _.pluck(toolPkgBuilds, '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];
});
process.stderr.write(
'Building bootstrap tarballs for architectures ' +
osArches.join(', ') + '\n');
// Before downloading anything, check that the catalog contains everything we
// need for the OSes that the tool is built for.
var messages = buildmessage.capture(function () {
_.each(osArches, function (osArch) {
_.each(release.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);
}
});
});
});
});
if (messages.hasMessages()) {
process.stderr.write("\n" + messages.formatMessages());
return 1;
};
files.mkdir_p(outputDirectory);
_.each(osArches, function (osArch) {
var tmpdir = files.mkdtemp();
// We're going to build and tar up a tropohouse in a temporary directory; we
// don't want to use any of our local packages, so we use catalog.official
// instead of catalog.
// XXX update to '.meteor' when we combine houses
var tmpTropo = new tropohouse.Tropohouse(
path.join(tmpdir, '.meteor0'), catalog.official);
var messages = buildmessage.capture(function () {
buildmessage.enterJob({
title: "downloading tool package " + toolPkg.package + "@" +
toolPkg.constraint
}, function () {
tmpTropo.maybeDownloadPackageForArchitectures(
{packageName: toolPkg.package, version: toolPkg.constraint},
[osArch], // XXX 'web.browser' too?
true);
});
_.each(release.packages, function (pkgVersion, pkgName) {
buildmessage.enterJob({
title: "downloading package " + pkgName + "@" + pkgVersion
}, function () {
tmpTropo.maybeDownloadPackageForArchitectures(
{packageName: pkgName, version: pkgVersion},
[osArch], // XXX 'web.browser' too?
true);
});
});
});
if (messages.hasMessages()) {
process.stderr.write("\n" + messages.formatMessages());
return 1;
}
// XXX should we include some sort of preliminary package-metadata as well?
// maybe with release info about the release we are using?
// Create the top-level 'meteor' symlink, which links to the latest tool's
// meteor shell script.
var toolUnipackagePath =
tmpTropo.packagePath(toolPkg.package, toolPkg.constraint);
var toolUnipackage = new unipackage.Unipackage;
toolUnipackage.initFromPath(toolPkg.package, toolUnipackagePath);
var toolRecord = _.findWhere(toolUnipackage.toolsOnDisk, {arch: osArch});
if (!toolRecord)
throw Error("missing tool for " + osArch);
fs.symlinkSync(
path.join(
tmpTropo.packagePath(toolPkg.package, toolPkg.constraint, true),
toolRecord.path,
'meteor'),
path.join(tmpTropo.root, 'meteor'));
files.createTarball(
tmpTropo.root,
path.join(outputDirectory, 'meteor-bootstrap-' + osArch + '.tar.gz'));
});
return 0;
});
///////////////////////////////////////////////////////////////////////////////
// admin set-banners
///////////////////////////////////////////////////////////////////////////////
// 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,
}, function (options) {
var bannersFile = options.args[0];
try {
var bannersData = fs.readFileSync(bannersFile, 'utf8');
bannersData = JSON.parse(bannersData);
} catch (e) {
process.stderr.write("Could not parse banners file: ");
process.stderr.write(e.message + "\n");
return 1;
}
if (!bannersData.track) {
process.stderr.write("Banners file should have a 'track' key.\n");
return 1;
}
if (!bannersData.banners) {
process.stderr.write("Banners file should have a 'banners' key.\n");
return 1;
}
try {
var conn = packageClient.loggedInPackagesConnection();
} catch (err) {
packageClient.handlePackageServerConnectionError(err);
return 1;
}
conn.call('setBannersOnReleases', bannersData.track,
bannersData.banners);
// Refresh afterwards.
catalog.official.refresh();
return 0;
});
///////////////////////////////////////////////////////////////////////////////
// self-test
///////////////////////////////////////////////////////////////////////////////
// XXX we should find a way to make self-test fully self-contained, so that it
// ignores "packageDirs" (ie, it shouldn't fail just because you happen to be
// sitting in an app with packages that don't build)
main.registerCommand({
name: 'self-test',
minArgs: 0,
maxArgs: 1,
options: {
changed: { type: Boolean },
'force-online': { type: Boolean },
slow: { type: Boolean },
browserstack: { type: Boolean },
history: { type: Number }
},
hidden: true
}, function (options) {
var selftest = require('./selftest.js');
// Auto-detect whether to skip 'net' tests, unless --force-online is passed.
var offline = false;
if (!options['force-online']) {
try {
require('./http-helpers.js').getUrl("http://www.google.com/");
} catch (e) {
if (e instanceof files.OfflineError)
offline = true;
}
}
var testRegexp = undefined;
if (options.args.length) {
try {
testRegexp = new RegExp(options.args[0]);
} catch (e) {
if (!(e instanceof SyntaxError))
throw e;
process.stderr.write("Bad regular expression: " + options.args[0] + "\n");
return 1;
}
}
var clients = {
browserstack: options.browserstack
};
return selftest.runTests({
onlyChanged: options.changed,
offline: offline,
includeSlowTests: options.slow,
historyLines: options.history,
clients: clients,
testRegexp: testRegexp
});
});
///////////////////////////////////////////////////////////////////////////////
// list-sites
///////////////////////////////////////////////////////////////////////////////
main.registerCommand({
name: 'list-sites',
minArgs: 0,
maxArgs: 0
}, function (options) {
auth.pollForRegistrationCompletion();
if (! auth.isLoggedIn()) {
process.stderr.write(
"You must be logged in for that. Try 'meteor login'.\n");
return 1;
}
return deploy.listSites();
});
///////////////////////////////////////////////////////////////////////////////
// dummy
///////////////////////////////////////////////////////////////////////////////
// Dummy test command. Used for automated testing of the command line
// option parser.
main.registerCommand({
name: 'dummy',
options: {
email: { type: String, short: "e", required: true },
port: { type: Number, short: "p", default: 3000 },
url: { type: Boolean, short: "U" },
'delete': { type: Boolean, short: "D" },
changed: { type: Boolean }
},
maxArgs: 2,
hidden: true
}, function (options) {
var p = function (key) {
if (_.has(options, key))
return JSON.stringify(options[key]);
return 'none';
};
process.stdout.write(p('email') + " " + p('port') + " " + p('changed') +
" " + p('args') + "\n");
if (options.url)
process.stdout.write('url\n');
if (options['delete'])
process.stdout.write('delete\n');
});