mirror of
https://github.com/meteor/meteor.git
synced 2026-01-10 08:08:24 -05:00
1597 lines
58 KiB
JavaScript
1597 lines
58 KiB
JavaScript
var showRequireProfile = ('METEOR_PROFILE_REQUIRE' in process.env);
|
||
if (showRequireProfile) {
|
||
require('../tool-env/profile-require.js').start();
|
||
}
|
||
|
||
var assert = require("assert");
|
||
var _ = require('underscore');
|
||
var Console = require('../console/console.js').Console;
|
||
var files = require('../fs/files');
|
||
var warehouse = require('../packaging/warehouse.js');
|
||
var tropohouse = require('../packaging/tropohouse.js');
|
||
var release = require('../packaging/release.js');
|
||
var projectContextModule = require('../project-context.js');
|
||
var catalog = require('../packaging/catalog/catalog.js');
|
||
var buildmessage = require('../utils/buildmessage.js');
|
||
var httpHelpers = require('../utils/http-helpers.js');
|
||
const { makeGlobalAsyncLocalStorage } = require("../utils/fiber-helpers");
|
||
const archinfo = require('../utils/archinfo');
|
||
import { isEmacs } from "../utils/utils.js";
|
||
|
||
var main = exports;
|
||
|
||
require('./flush-buffers-on-exit-in-windows.js');
|
||
|
||
// node (v8) defaults to only recording 10 lines of stack trace. This
|
||
// is especially insufficient when using fibers, because you get
|
||
// proper call stacks instead of only seeing the stack up to the most
|
||
// recent callback invocation. Increase the limit (for the `meteor` tool
|
||
// itself, not for apps).
|
||
//
|
||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||
Error.stackTraceLimit = Infinity;
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Command registration
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
function Command(options) {
|
||
assert.ok(this instanceof Command);
|
||
|
||
options = Object.assign({
|
||
minArgs: 0,
|
||
options: {},
|
||
allowUnrecognizedOptions: false,
|
||
requiresApp: false,
|
||
requiresPackage: false,
|
||
requiresAppOrPackage: false,
|
||
requiresRelease: true,
|
||
hidden: false,
|
||
pretty: true,
|
||
notOnWindows: false
|
||
}, options);
|
||
|
||
if (! _.has(options, 'maxArgs')) {
|
||
options.maxArgs = options.minArgs;
|
||
}
|
||
|
||
_.each(["name", "func"], function (key) {
|
||
if (! _.has(options, key)) {
|
||
throw new Error("command missing '" + key + "'?");
|
||
}
|
||
});
|
||
|
||
Object.assign(this, options);
|
||
|
||
_.each(this.options, function (value, key) {
|
||
if (key === "args" || key === "appDir") {
|
||
throw new Error(options.name + ": bad option name " + key);
|
||
}
|
||
if (! _.has(value, 'type')) {
|
||
value.type = String;
|
||
}
|
||
if (_.has(value, 'default') && _.has(value, 'required')) {
|
||
throw new Error(options.name + ": " + key + " can't be both optional " +
|
||
"and required");
|
||
}
|
||
if (_.has(value, 'short') && value.short.length !== 1) {
|
||
throw new Error(options.name + ": " + key + " has a bad short option");
|
||
}
|
||
});
|
||
};
|
||
|
||
// Various registerCommand options such as requiresApp can be specified to
|
||
// either as a constant value or as a function dependent on the parsed
|
||
// command-line options object.
|
||
Command.prototype.evaluateOption = function (optionName, options) {
|
||
var self = this;
|
||
if (typeof self[optionName] === 'function') {
|
||
return self[optionName](options);
|
||
}
|
||
return self[optionName];
|
||
};
|
||
|
||
// map from command name to a Command, or to a subcommand map (a map
|
||
// of subcommand names to either Commands or further submaps).
|
||
//
|
||
// Options that function as commands (eg, "meteor --arch") are treated
|
||
// as subcommands of "--".
|
||
var commands = {};
|
||
|
||
// Exception to throw from a command to bail out and show command
|
||
// usage information.
|
||
main.ShowUsage = function ShowUsage() {
|
||
assert.ok(this instanceof ShowUsage);
|
||
};
|
||
|
||
// Exception to throw from a helper function inside a command which is identical
|
||
// to returning the given exit code from the command. ONLY USE THIS IN HELPERS
|
||
// THAT ARE ONLY CALLED DIRECTLY FROM COMMANDS! DON'T BE LAZY AND PUT THROW OF
|
||
// THIS IN RANDOM LIBRARY CODE!
|
||
main.ExitWithCode = function ExitWithCode(code) {
|
||
assert.ok(this instanceof ExitWithCode);
|
||
this.code = code;
|
||
};
|
||
|
||
Object.assign(main.ExitWithCode.prototype, {
|
||
toString: function () {
|
||
var self = this;
|
||
return "ExitWithCode:" + self.code;
|
||
}
|
||
});
|
||
|
||
// Exception to throw to skip the process.exit call.
|
||
main.WaitForExit = function WaitForExit() {
|
||
assert.ok(this instanceof WaitForExit);
|
||
};
|
||
|
||
// Exception to throw from a command to exit, restart, and reinvoke
|
||
// the command with the latest available (downloaded) Meteor release.
|
||
// If track is specified, it uses the latest available in the given
|
||
// track instead of the default track.
|
||
main.SpringboardToLatestRelease =
|
||
function SpringboardToLatestRelease(track) {
|
||
assert.ok(this instanceof SpringboardToLatestRelease);
|
||
this.track = track;
|
||
};
|
||
|
||
// Exception to throw from a command to exit, restart, and reinvoke
|
||
// the command with the given Meteor release.
|
||
main.SpringboardToSpecificRelease =
|
||
function SpringboardToSpecificRelease(fullReleaseName, msg) {
|
||
assert.ok(this instanceof SpringboardToSpecificRelease);
|
||
this.fullReleaseName = fullReleaseName;
|
||
this.msg = msg;
|
||
};
|
||
|
||
// Register a command-line command.
|
||
//
|
||
// options:
|
||
// - name
|
||
// - can be a basic command, like "deploy"
|
||
// - can be a subcommand, like "admin grant"
|
||
// (distinguished by presence of ' ')
|
||
// - can be an option that functions as a command, like "--arch"
|
||
// (distinguished by starting with '--')
|
||
// - minArgs: minimum non-option arguments that can be present (default 0)
|
||
// - maxArgs: maximum non-option arguments that can be present (defaults to
|
||
// whatever value you passed for minArgs; use Infinity for unlimited)
|
||
// - catalogRefresh: strategy object specifying when to refresh the catalog.
|
||
// - options: map from long option name to:
|
||
// - type: String, Number, or Boolean. default is String. a future
|
||
// version could support [String] and [Number] to allow the option to
|
||
// be passed more than once, but we don't do that yet.
|
||
// - short: single character short alias (eg, 'p' for 'port', to do -p 3000)
|
||
// - default: value to use if none supplied
|
||
// - required: true if required (incompatible with 'default')
|
||
// - requiresApp: does this command work with an app? possible values
|
||
// (defaults to false):
|
||
// - true if an app is required, and command must be run inside an
|
||
// app. The command will be run using the app's Meteor release
|
||
// (unless overridden by --release or a checkout). An 'appDir'
|
||
// option will be passed with the absolute path to the app's
|
||
// top-level directory, and an error will be printed if the
|
||
// command isn't run from inside an app.
|
||
// - false if an app is not required. But if the command does happen
|
||
// to have been run from an app, 'appDir' will be
|
||
// provided. Moreover, in that case, we will still use the version
|
||
// of this program that goes with the Meteor release of the
|
||
// app. This is not ideal but is necessary for 'meteor help' to
|
||
// behave in a sane way in our current system. (XXX In the future
|
||
// we should separate the build system out into a package that is
|
||
// versioned with the release, and then take the CLI tool out of
|
||
// the release and always use the latest available version.)
|
||
// - function: some apps determine whether they use an app based on
|
||
// their arguments (eg, 'deploy' versus 'deploy --delete'). for
|
||
// these, set requiresApp to a function that takes 'options' (same as
|
||
// would be received by the actual command function) and returns
|
||
// true or false.
|
||
// - requiresPackage: defaults to false.
|
||
// - requiresAppOrPackage: a combination of two options above, true/false.
|
||
// - requiresRelease: defaults to true. Set to false if this command
|
||
// doesn't need a functioning Meteor release to be available (that
|
||
// is, if the command does not need the ability to resolve
|
||
// packages). There is only one case where this comes up: if you
|
||
// create an app with a checkout (so that it has no release), and
|
||
// then run that app with released Meteor. Normally this just prints
|
||
// an error saying that you have to pick a release, but you can
|
||
// disable that by setting this flag to false. Even if you set this
|
||
// flag, we will still *attempt* to run the correct Meteor release
|
||
// just like we always do; it's just that in that one case, instead
|
||
// of bailing out with an error we will run your command with
|
||
// release.current === null.
|
||
// - hidden: do not show in command list in help
|
||
//
|
||
// An error will be printed if an unrecognized option is passed on the
|
||
// command line (eg, '--foo' when you don't have a 'foo' key in
|
||
// options.options), or a required option is missing, or the number of
|
||
// other arguments isn't as required by minArgs / maxArgs.
|
||
//
|
||
// func: function to call when the command is chosen. receives one
|
||
// argument, an options dictionary that contains:
|
||
// - the values of any 'options' that were provided
|
||
// - args: an array of the other command-line arguments
|
||
// - appDir: if run from inside an app tree, the absolute path to the
|
||
// app's top-level directory
|
||
//
|
||
// func should do one of the following:
|
||
// - On success, return undefined (or 0). This indicates successful
|
||
// completion, and the program will exit with status 0.
|
||
// - On failure, return a positive number. The program will exit with that
|
||
// status.
|
||
// - If the command-line arguments aren't valid, 'throw new
|
||
// main.ShowUsage'. This will print usage info for the command and
|
||
// exit with status 1.
|
||
// - If you have started (for example) a subprocess or worker fiber
|
||
// and want to wait until it's finished to exit, 'throw new
|
||
// main.WaitForExit'. This will skip the call to process.exit and the
|
||
// program will keep running until node thinks that everything is
|
||
// done.
|
||
// - To quit, restart, and rerun the command with a latest available
|
||
// (downloaded) Meteor release, 'throw new main.SpringboardToLatestRelease'.
|
||
//
|
||
// Commands should never call process.exit()! They should instead
|
||
// return an appropriate value.
|
||
|
||
main.registerCommand = function (options, func) {
|
||
options = _.clone(options);
|
||
options.func = func;
|
||
|
||
var nameParts = options.name.trim().split(/\s+/);
|
||
options.name = nameParts.join(' ');
|
||
|
||
if (nameParts[0].indexOf('--') === 0) {
|
||
// "--foo" -> "--" "foo"
|
||
nameParts[0] = nameParts[0].substr(2);
|
||
nameParts.unshift('--');
|
||
}
|
||
|
||
var target = commands;
|
||
while (nameParts.length > 1) {
|
||
var part = nameParts.shift();
|
||
if (! _.has(target, part)) {
|
||
target[part] = {};
|
||
}
|
||
target = target[part];
|
||
}
|
||
|
||
if (_.has(target, nameParts[0])) {
|
||
throw Error("Duplicate command: " + options.name);
|
||
}
|
||
|
||
if (!options.catalogRefresh) {
|
||
throw Error("Command does not select a catalogRefresh strategy: " +
|
||
options.name);
|
||
}
|
||
|
||
target[nameParts[0]] = new Command(options);
|
||
};
|
||
|
||
main.captureAndExit = async function (header, title, f) {
|
||
var messages;
|
||
if (f) {
|
||
messages = await buildmessage.capture({ title: title }, f);
|
||
} else {
|
||
messages = await buildmessage.capture(title); // title is really f
|
||
}
|
||
if (messages.hasMessages()) {
|
||
Console.error(header);
|
||
Console.printMessages(messages);
|
||
throw new main.ExitWithCode(1);
|
||
}
|
||
};
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Load all the commands
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
// NB: files required up to this point may not define commands
|
||
|
||
const { isModernWatcherEnabled } = require('./commands.js');
|
||
require('./commands-packages.js');
|
||
require('./commands-packages-query.js');
|
||
require('./commands-cordova.js');
|
||
require('./commands-aliases.js');
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Record all the top-level commands as JSON
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
export const meteorCommandsJsonPath = files.pathJoin(
|
||
files.getDevBundle(), "bin", ".meteor-commands.json"
|
||
);
|
||
|
||
export function dumpMeteorCommands() {
|
||
const all = Object.create(null);
|
||
Object.keys(commands).forEach(name => all[name] = true);
|
||
const json = JSON.stringify(all, null, 2);
|
||
files.writeFile(meteorCommandsJsonPath, json + "\n");
|
||
return all;
|
||
}
|
||
|
||
if (files.inCheckout()) {
|
||
// If we're running Meteor from a checkout, dump the commands every
|
||
// time, so that the file remains up to date.
|
||
dumpMeteorCommands();
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Long-form help
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
// Returns an array of entries with keys:
|
||
// - name (entry name, typically a command name)
|
||
// - body (contents of body, trimmed to end with a newline but no blank lines)
|
||
var loadHelp = function () {
|
||
var dirname = files.convertToStandardPath(__dirname);
|
||
var raw = files.readFile(files.pathJoin(dirname, 'help.txt'), 'utf8');
|
||
return _.map(raw.split(/^>>>/m).slice(1), function (r) {
|
||
var lines = r.split('\n');
|
||
var name = lines.shift().trim();
|
||
return {
|
||
name: name,
|
||
body: lines.join('\n').replace(/\s*$/, '') + '\n'
|
||
};
|
||
});
|
||
};
|
||
|
||
var longHelp = exports.longHelp = function (commandName) {
|
||
commandName = commandName.trim();
|
||
var parts = commandName.length ? commandName.split(' ') : [];
|
||
var node = commands;
|
||
_.each(parts, function (part) {
|
||
if (! _.has(node, part)) {
|
||
throw new Error("walked off edge of command tree?");
|
||
}
|
||
node = node[part];
|
||
});
|
||
|
||
var help = loadHelp();
|
||
|
||
// can use to see if there is help text for a particular command
|
||
var helpDict = {};
|
||
_.each(help, function (helpEntry) {
|
||
helpDict[helpEntry.name] = helpEntry;
|
||
});
|
||
|
||
var commandList = null;
|
||
if (! (node instanceof Command)) {
|
||
commandList = '';
|
||
var commandsWanted = {};
|
||
|
||
_.each(node, function (n, shortName) {
|
||
var fullName = commandName + (commandName.length > 0 ? " " : "") +
|
||
shortName;
|
||
|
||
// Use helpDict to only include commands that have help text, otherwise
|
||
// there is nothing to display.
|
||
// For now, there's no way to mark commands with subcommands (eg 'admin')
|
||
// as hidden.
|
||
if (! n.hidden && helpDict[fullName]) {
|
||
commandsWanted[fullName] = { name: shortName };
|
||
}
|
||
});
|
||
|
||
var maxNameLength = _.max(_.map(commandsWanted, function (c) {
|
||
return c.name.length;
|
||
}));
|
||
|
||
// Assemble help text for subcommands.. in the order they appear
|
||
// in the help file
|
||
_.each(help, function (helpEntry) {
|
||
if (_.has(commandsWanted, helpEntry.name)) {
|
||
var shortName = commandsWanted[helpEntry.name].name;
|
||
commandList += " " + shortName +
|
||
new Array(maxNameLength + 1).join(' ').substr(shortName.length) +
|
||
" " + helpEntry.body.split('\n')[0] + "\n";
|
||
}
|
||
});
|
||
|
||
// Remove trailing newline so that you can write "{{commands}}" on
|
||
// a line by itself and it does what you think it would
|
||
commandList = commandList.substr(0, commandList.length - 1);
|
||
}
|
||
|
||
var entry = _.find(help, function (c) {
|
||
return c.name === commandName;
|
||
});
|
||
if (! entry) {
|
||
throw new Error("help missing for " + commandName + "?");
|
||
}
|
||
var ret = entry.body.split('\n').slice(1).join('\n');
|
||
if (commandList !== null) {
|
||
ret = ret.replace('{{commands}}', commandList);
|
||
}
|
||
|
||
return ret;
|
||
};
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Springboarding
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
// Exit and restart the program, with the same arguments, but using a
|
||
// different version of the tool and/or forcing a particular release.
|
||
//
|
||
// - release: required. the version of the tool to run.
|
||
//
|
||
// options:
|
||
// - releaseOverride: optional. if provided, a release name to force
|
||
// us to use when restarting (this functions exactly like --release
|
||
// and will cause release.forced to be true).
|
||
// - fromApp: this release was suggested because it is the app's
|
||
// release. affects error messages.
|
||
var springboard = async function (rel, options) {
|
||
options = options || {};
|
||
if (process.env.METEOR_DEBUG_SPRINGBOARD) {
|
||
console.log("WILL SPRINGBOARD TO", rel.getToolsPackageAtVersion());
|
||
}
|
||
|
||
const toolsPkg = rel.getToolsPackage();
|
||
const toolsVersion = rel.getToolsVersion();
|
||
const serverArchitectures = await catalog.official.filterArchesWithBuilds(
|
||
toolsPkg,
|
||
toolsVersion,
|
||
archinfo.acceptableMeteorToolArches(),
|
||
);
|
||
|
||
if (serverArchitectures.length === 0) {
|
||
var release = await catalog.official.getDefaultReleaseVersion();
|
||
var releaseName = release.track + "@" + release.version;
|
||
|
||
Console.error(
|
||
"This project uses " + rel.getDisplayName() + ", which isn't",
|
||
"available on this platform. To work with this app on all supported",
|
||
"platforms, use", Console.command("meteor update --release " + releaseName),
|
||
"to pin this app to the newest compatible release."
|
||
);
|
||
|
||
process.exit(1);
|
||
}
|
||
|
||
const packageMapModule = require('../packaging/package-map.js');
|
||
const packageMap = new packageMapModule.PackageMap({
|
||
[toolsPkg]: toolsVersion,
|
||
});
|
||
|
||
// XXX split better
|
||
await Console.withProgressDisplayVisible(async function () {
|
||
var messages = await buildmessage.capture({
|
||
title: "downloading the command-line tool"
|
||
}, async function () {
|
||
await catalog.runAndRetryWithRefreshIfHelpful(async function () {
|
||
await tropohouse.default.downloadPackagesMissingFromMap(packageMap, {
|
||
serverArchitectures,
|
||
});
|
||
});
|
||
});
|
||
|
||
if (messages.hasMessages()) {
|
||
// We have failed to download the tool that we are supposed to springboard
|
||
// to! That's bad. Let's exit.
|
||
if (options.fromApp) {
|
||
Console.error(
|
||
"Sorry, this project uses " + rel.getDisplayName() + ", which is",
|
||
"not installed and could not be downloaded. Please check to make",
|
||
"sure that you are online.");
|
||
} else {
|
||
Console.error(
|
||
"Sorry, " + rel.getDisplayName() + " is not installed and could not",
|
||
"be downloaded. Please check to make sure that you are online.");
|
||
}
|
||
|
||
process.exit(1);
|
||
}
|
||
});
|
||
|
||
const isopack = require('../isobuild/isopack.js');
|
||
const packagePath = tropohouse.default.packagePath(toolsPkg, toolsVersion);
|
||
const toolIsopack = new isopack.Isopack;
|
||
await toolIsopack.initFromPath(toolsPkg, packagePath);
|
||
|
||
let toolRecord = null;
|
||
serverArchitectures.some(arch => {
|
||
return toolRecord = _.findWhere(toolIsopack.toolsOnDisk, { arch });
|
||
});
|
||
|
||
if (!toolRecord) {
|
||
throw Error("missing tool for " + archinfo.host() + " in " +
|
||
toolsPkg + "@" + toolsVersion);
|
||
}
|
||
|
||
const newToolsDir = files.pathJoin(packagePath, toolRecord.path);
|
||
if (files.realpath(newToolsDir) ===
|
||
files.realpath(files.getCurrentToolsDir())) {
|
||
if (options.mayReturn) {
|
||
// Return instead of springboarding, if we are allowed to keep using
|
||
// the current tools without restarting the process.
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Strip off the "node" and "meteor.js" from argv and replace it with the
|
||
// appropriate tools's meteor shell script.
|
||
var newArgv = [];
|
||
const argc = process.argv.length;
|
||
for (var i = 2; i < argc; ++i) {
|
||
const arg = process.argv[i];
|
||
if (arg === "--unsafe-perm" ||
|
||
arg === "--allow-superuser") {
|
||
// Don't pass the --unsafe-perm or --allow-superuser flags to
|
||
// springboarded versions since they may not know how to use them,
|
||
// but set the METEOR_ALLOW_SUPERUSER environment variable in case
|
||
// the springboarded version needs it. See meteor/meteor#7959.
|
||
if (! _.has(process.env, "METEOR_ALLOW_SUPERUSER")) {
|
||
process.env.METEOR_ALLOW_SUPERUSER = "true";
|
||
}
|
||
continue;
|
||
}
|
||
newArgv.push(arg);
|
||
}
|
||
|
||
if (_.has(options, 'releaseOverride')) {
|
||
// We used to just append --release=<releaseOverride> to the arguments, and
|
||
// though that's probably safe in practice, it makes us worry about things
|
||
// like other --release options. So now we use an environment
|
||
// variable. #SpringboardEnvironmentVar
|
||
process.env['METEOR_SPRINGBOARD_RELEASE'] = options.releaseOverride;
|
||
}
|
||
|
||
// Release our connection to the sqlite catalog database for the current
|
||
// process, so that the springboarded process can reestablish it.
|
||
await catalog.official.closePermanently();
|
||
|
||
const isWindows = process.platform === "win32";
|
||
const executable = files.pathJoin(
|
||
newToolsDir,
|
||
isWindows ? "meteor.bat" : "meteor",
|
||
);
|
||
|
||
if (isWindows) {
|
||
process.exit(await new Promise(function (resolve) {
|
||
var execPath = files.convertToOSPath(executable);
|
||
var child = require("child_process").spawn(execPath, newArgv, {
|
||
env: process.env,
|
||
stdio: 'inherit',
|
||
shell: true,
|
||
}).on('exit', resolve);
|
||
}));
|
||
}
|
||
|
||
// Now exec; we're not coming back.
|
||
require('kexec')(executable, newArgv);
|
||
throw Error('exec failed?');
|
||
};
|
||
|
||
// Springboard to a pre-0.9.0 release.
|
||
var oldSpringboard = async function (toolsVersion) {
|
||
// Strip off the "node" and "meteor.js" from argv and replace it with the
|
||
// appropriate tools's meteor shell script.
|
||
var newArgv = process.argv.slice(2);
|
||
var cmd =
|
||
files.pathJoin(warehouse.getToolsDir(toolsVersion), 'bin', 'meteor');
|
||
|
||
// Release our connection to the sqlite catalog database for the current
|
||
// process, so that the springboarded process can reestablish it.
|
||
await catalog.official.closePermanently();
|
||
|
||
// Now exec; we're not coming back.
|
||
require('kexec')(cmd, newArgv);
|
||
throw Error('exec failed?');
|
||
};
|
||
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// Main entry point
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
|
||
// This is the main function that runs when you type 'meteor'.
|
||
|
||
// It's mostly concerned with validating command-line arguments,
|
||
// finding the requested command in the commands table, and making
|
||
// sure that you're using the version of the Meteor tools that match
|
||
// your project.
|
||
makeGlobalAsyncLocalStorage().run({}, async function () {
|
||
// If running inside the Emacs shell, set stdin to be blocking,
|
||
// reversing node's normal setting of O_NONBLOCK on the evaluation
|
||
// of process.stdin (because Node unblocks stdio when forking). This
|
||
// fixes execution of Mongo from within Emacs shell.
|
||
if (isEmacs()) {
|
||
process.stdin;
|
||
var child_process = require('child_process');
|
||
child_process.spawn('true', [], {stdio: 'inherit'});
|
||
}
|
||
|
||
// Check required Node version.
|
||
// This code is duplicated in tools/server/boot.js.
|
||
var MIN_NODE_VERSION = 'v18.16.0';
|
||
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
|
||
Console.error(
|
||
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.');
|
||
process.exit(1);
|
||
}
|
||
|
||
// This is a bit of a hack, but: if we don't check this in the tool, then the
|
||
// first time we do a isopack.load, it will fail due to the check in the
|
||
// meteor package, and that'll look a lot uglier.
|
||
if (process.env.ROOT_URL) {
|
||
var parsedUrl = require('url').parse(process.env.ROOT_URL);
|
||
if (!parsedUrl.host || ['http:', 'https:'].indexOf(parsedUrl.protocol) === -1) {
|
||
Console.error('$ROOT_URL, if specified, must be an URL.');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Parse the arguments.
|
||
//
|
||
// We must first identify which options are boolean and which take
|
||
// arguments (which must be consistent across all defined
|
||
// commands). This is necessary to resolve cases like 'meteor --flag
|
||
// stuff thing'. Is the command 'stuff' with a boolean option
|
||
// 'flag', or in the command 'thing' with an option 'flag' that is
|
||
// set to 'stuff'? To resolve this we require that 'flag' be
|
||
// consistently declared as a boolean (or not a boolean) across all
|
||
// commands.
|
||
//
|
||
// XXX The problem with the above is that which commands are boolean
|
||
// may change across releases, and when we springboard, we actually
|
||
// have to parse the options with the *target* version's
|
||
// semantics. All in all, I think we might be better served to
|
||
// require options to come after the command, other than special
|
||
// options (--release, --help, and options that act as
|
||
// commands). Then we don't have to require consistency of boolean
|
||
// status between commands; we instead have to require consistency
|
||
// of boolean status of a particular option, for a command, across
|
||
// releases. Since we always start out by running the latest version
|
||
// of Meteor, which can have knowledge of all past versions
|
||
// (including the boolean status of formerly present but removed
|
||
// options, including options to removed commands), this should let
|
||
// us be 100% correct. (Of course, we could still do this if we
|
||
// required options to be consistent across commands as well, but I
|
||
// think this is a better tradeoff.) In this model, we'd do option
|
||
// parsing in two passes, where the first pass just pulls out the
|
||
// command, and the second parses the arguments with knowledge of
|
||
// the command. I would make this change right now but we're on a
|
||
// tight timetable for 1.0 and there is no advantage to doing it now
|
||
// rather than later. #ImprovingCrossVersionOptionParsing
|
||
|
||
const implicitValues = Object.create(null);
|
||
|
||
var isBoolean = {
|
||
"--help": true,
|
||
"--unsafe-perm": true,
|
||
"--allow-superuser": true,
|
||
};
|
||
|
||
var walkCommands = function (node) {
|
||
_.each(node, function (value, key) {
|
||
if (value instanceof Command) {
|
||
_.each(value.options || {}, function (optionInfo, optionName) {
|
||
var names = ["--" + optionName];
|
||
if (_.has(optionInfo, 'short')) {
|
||
names.push("-" + optionInfo.short);
|
||
}
|
||
_.each(names, function (name) {
|
||
if (_.has(optionInfo, "implicitValue")) {
|
||
implicitValues[name] = optionInfo.implicitValue;
|
||
}
|
||
var optionIsBoolean = (optionInfo.type === Boolean);
|
||
if (_.has(isBoolean, name)) {
|
||
if (isBoolean[name] !== optionIsBoolean) {
|
||
throw new Error("conflict: option '" + name + "' is used " +
|
||
"both as a boolean and as another type for " +
|
||
"command " + key);
|
||
}
|
||
} else {
|
||
isBoolean[name] = optionIsBoolean;
|
||
}
|
||
});
|
||
});
|
||
} else {
|
||
walkCommands(value);
|
||
}
|
||
});
|
||
};
|
||
walkCommands(commands);
|
||
|
||
// This is for things like '--arch' and '--version' which look like
|
||
// options, but actually function pretty much like commands. That's
|
||
// a little weird but it feels good and it follows a grand Unix
|
||
// tradition.
|
||
_.each(commands['--'] || {}, function (value, key) {
|
||
if (_.has(isBoolean, "--" + key)) {
|
||
throw new Error("--" + key + " is both an option and a command?");
|
||
}
|
||
isBoolean["--" + key] = true;
|
||
});
|
||
|
||
// Now parse!
|
||
var argv = process.argv.slice(2);
|
||
var rawOptions = {}; // map from '--foo' or '-f' to array of values
|
||
var rawArgs = [];
|
||
for (var i = 0; i < argv.length; i++) {
|
||
var term = argv[i];
|
||
|
||
// --: stop-parsing marker
|
||
if (term === "--") {
|
||
// Remainder is unparsed
|
||
rawArgs = rawArgs.concat(argv.slice(i + 1));
|
||
break;
|
||
}
|
||
|
||
// -: just an argument named '-'
|
||
if (term === "-") {
|
||
rawArgs.push(term);
|
||
continue;
|
||
}
|
||
|
||
if (term.match(/^--?=/)) {
|
||
Console.error("Option names cannot begin with '='.");
|
||
process.exit(1);
|
||
}
|
||
|
||
// A single option, like --foo or -f
|
||
if (term.match(/^--/) || term.match(/^-.$/)) {
|
||
var value = undefined;
|
||
|
||
// Split the term (once only!) on an equal sign.
|
||
var equals = term.indexOf('=');
|
||
if (equals !== -1) {
|
||
value = term.substr(equals + 1);
|
||
term = term.substr(0, equals);
|
||
}
|
||
|
||
if (! _.has(rawOptions, term)) {
|
||
rawOptions[term] = [];
|
||
}
|
||
|
||
// Save off the value of the option. true for (known) booleans,
|
||
// null if value is missing, else a string. Don't try to
|
||
// validate or interpret it yet.
|
||
if (isBoolean[term]) {
|
||
// If we got an '=' for a boolean, this is an error, which will be
|
||
// printed prettily later if we push false here.
|
||
rawOptions[term].push(value === undefined);
|
||
} else if (value !== undefined) {
|
||
// Handle '--foo=bar' and '--foo=' (which means "set to empty string").
|
||
rawOptions[term].push(value);
|
||
} else if (_.has(implicitValues, term)) {
|
||
rawOptions[term].push(implicitValues[term]);
|
||
} else if (i === argv.length - 1) {
|
||
rawOptions[term].push(null);
|
||
} else {
|
||
rawOptions[term].push(argv[i + 1]);
|
||
i ++;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Compound short option ('-abc', '-p45', '-abcp45')? Rewrite it
|
||
// in place into '-a -b -c', '-p 45', '-a -b -c -p 45'. Not that
|
||
// anyone really talks this way anymore.
|
||
if (term.match(/^-/)) {
|
||
if (term.match(/^-[-=]?$/)) {
|
||
throw Error("these cases should be handled above?");
|
||
}
|
||
|
||
var replacements = [];
|
||
for (var j = 1; j < term.length; j++) {
|
||
var subterm = "-" + term.charAt(j);
|
||
if (isBoolean[subterm] === false) {
|
||
// If we recognize this short option, and we're sure that it
|
||
// takes a value, and there are remaining characters in the
|
||
// short option, then those remaining characters are the value.
|
||
replacements.push(subterm);
|
||
var remainder = term.substr(j + 1);
|
||
if (remainder.length) {
|
||
// If there's an '=' here, don't include it in the option value. A
|
||
// trailing '=' *should* cause us to set the option value to ''.
|
||
if (remainder.charAt(0) === '=') {
|
||
remainder = remainder.substr(1);
|
||
}
|
||
replacements.push(remainder);
|
||
break;
|
||
}
|
||
} else if (isBoolean[subterm] &&
|
||
j + 1 < term.length && term.charAt(j + 1) === '=') {
|
||
// We know it's a boolean, but we've been given an '='. This will
|
||
// cause a pretty error later.
|
||
if (! _.has(rawOptions, subterm)) {
|
||
rawOptions[subterm] = [];
|
||
}
|
||
rawOptions[subterm].push(false);
|
||
// Don't process the '=' on the next pass.
|
||
j ++;
|
||
} else {
|
||
// It's a boolean without an '=', or it's something we've never heard
|
||
// of. (In the latter case, assume it's boolean for now, and we'll
|
||
// print an error later.)
|
||
replacements.push(subterm);
|
||
}
|
||
}
|
||
|
||
_.partial(argv.splice, i, 1).apply(argv, replacements);
|
||
i --;
|
||
continue;
|
||
}
|
||
|
||
// It is a plain old argument!
|
||
rawArgs.push(term);
|
||
}
|
||
|
||
if (_.has(rawOptions, "--allow-superuser") ||
|
||
_.has(rawOptions, "--unsafe-perm")) {
|
||
process.env.METEOR_ALLOW_SUPERUSER = "true";
|
||
delete rawOptions["--allow-superuser"];
|
||
delete rawOptions["--unsafe-perm"];
|
||
}
|
||
|
||
// Prevent running meteor as root on UNIX platforms.
|
||
if (process.getuid &&
|
||
process.getuid() === 0) {
|
||
const allowSuperuser = !! (
|
||
process.env.METEOR_ALLOW_SUPERUSER &&
|
||
JSON.parse(process.env.METEOR_ALLOW_SUPERUSER));
|
||
|
||
if (! allowSuperuser) {
|
||
// Meteor is running as root without METEOR_ALLOW_SUPERUSER, notice and stop.
|
||
Console.error("");
|
||
Console.error(
|
||
"You are attempting to run Meteor as the 'root' superuser.",
|
||
"If you are developing, this is almost certainly *not* what you want to do and will likely result in incorrect file permissions.",
|
||
"However, if you are running this command in a build process (CI, etc.), or you are absolutely sure you know what you are doing,",
|
||
"set the METEOR_ALLOW_SUPERUSER environment variable or pass --allow-superuser to proceed."
|
||
);
|
||
}
|
||
|
||
Console.info("");
|
||
Console.info(
|
||
"Even with METEOR_ALLOW_SUPERUSER or --allow-superuser, permissions in your app directory will be incorrect if you ever attempt to perform any Meteor tasks as a normal user.",
|
||
"If you need to fix your permissions, run the following command from the root of your project:"
|
||
);
|
||
Console.info("");
|
||
Console.info(Console.command(" sudo chown -Rh <username> .meteor/local"));
|
||
Console.info("");
|
||
|
||
if (! allowSuperuser) {
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Figure out if we're running in a directory that is part of a Meteor
|
||
// application or package. Determine any additional directories to
|
||
// search for packages.
|
||
|
||
var appDir = files.findAppDir();
|
||
if (appDir) {
|
||
appDir = files.pathResolve(appDir);
|
||
}
|
||
global.modernWatcher = isModernWatcherEnabled(appDir);
|
||
|
||
await require('../tool-env/isopackets.js').ensureIsopacketsLoadable();
|
||
|
||
// Initialize the server catalog. Among other things, this is where we get
|
||
// release information (used by springboarding). We do not at this point talk
|
||
// to the server and refresh it.
|
||
await catalog.official.initialize({
|
||
offline: !!process.env.METEOR_OFFLINE_CATALOG
|
||
});
|
||
|
||
// Now before we do anything else, figure out the release to use,
|
||
// and if that release goes with a different version of the tools,
|
||
// quit and run those tools instead.
|
||
//
|
||
// Note that doing this correctly requires knowledge of which
|
||
// arguments are boolean (in 'meteor --option --release 1.0', is
|
||
// '--release' a flag or the values of '--option')? We have to use
|
||
// the flag definitions in the current (latest) version of meteor to
|
||
// decide whether to exec the other version of meteor that would
|
||
// interpret the flags. That's not ideal, but it should do fine in
|
||
// practice, and it's better than assuming that all options are or
|
||
// aren't boolean when interpreting --release. See
|
||
// #ImprovingCrossVersionOptionParsing.
|
||
|
||
var releaseOverride = null;
|
||
var releaseForced = false;
|
||
var releaseExplicit = false;
|
||
var releaseFromApp = false;
|
||
if (_.has(rawOptions, '--release')) {
|
||
if (rawOptions['--release'].length > 1) {
|
||
Console.error(
|
||
"--release should only be passed once. " +
|
||
"Try 'meteor help' for help.");
|
||
process.exit(1);
|
||
}
|
||
releaseOverride = rawOptions['--release'][0];
|
||
releaseForced = true;
|
||
releaseExplicit = true;
|
||
if (! releaseOverride) {
|
||
Console.error(
|
||
"The --release option needs a value. " +
|
||
"Try 'meteor help' for help.");
|
||
process.exit(1);
|
||
}
|
||
delete rawOptions['--release'];
|
||
}
|
||
|
||
if (_.has(process.env, 'METEOR_SPRINGBOARD_RELEASE')) {
|
||
// See #SpringboardEnvironmentVar
|
||
// Note that this causes release.forced to be true, but not
|
||
// release.explicit. release.forced means "we're using
|
||
// some sort of externally specified release, not the app
|
||
// release"; release.explicit means "the end-user typed
|
||
// --release".
|
||
releaseOverride = process.env['METEOR_SPRINGBOARD_RELEASE'];
|
||
releaseForced = true;
|
||
}
|
||
|
||
var releaseName, appReleaseFile;
|
||
|
||
if (appDir) {
|
||
appReleaseFile = new projectContextModule.ReleaseFile({
|
||
projectDir: appDir
|
||
});
|
||
await appReleaseFile.init();
|
||
// This is what happens if the file exists and is empty. This really
|
||
// shouldn't happen unless the user did it manually.
|
||
if (appReleaseFile.noReleaseSpecified()) {
|
||
Console.error(
|
||
"Problem! This project has a .meteor/release file which is empty.",
|
||
"The file should either contain the release of Meteor that you want",
|
||
"to use, or the word 'none' if you will only use the project with",
|
||
"unreleased checkouts of Meteor. Please edit the .meteor/release",
|
||
"file in the project and change it to a valid Meteor release or",
|
||
"'none'.");
|
||
process.exit(1);
|
||
} else if (appReleaseFile.fileMissing()) {
|
||
Console.error(
|
||
"Problem! This project does not have a .meteor/release file.",
|
||
"The file should either contain the release of Meteor that you",
|
||
"want to use, or the word 'none' if you will only use the project",
|
||
"with unreleased checkouts of Meteor. Please edit the",
|
||
".meteor/release file in the project and change it to a valid Meteor",
|
||
"release or 'none'.");
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
if (! files.usesWarehouse()) {
|
||
// Running from a checkout
|
||
if (releaseOverride) {
|
||
Console.error(
|
||
"Can't specify a release when running Meteor from a checkout.");
|
||
process.exit(1);
|
||
}
|
||
releaseName = null;
|
||
} else {
|
||
// Running from an install
|
||
if (releaseOverride) {
|
||
// Use the release explicitly specified on the command line.
|
||
releaseName = releaseOverride;
|
||
} else if (appDir) {
|
||
// Running from an app directory. Use release specified by app.
|
||
if (appReleaseFile.isCheckout()) {
|
||
// Looks like we don't have a release. Leave release.current === null.
|
||
} else {
|
||
// Use the project's desired release
|
||
releaseName = appReleaseFile.unnormalizedReleaseName;
|
||
releaseFromApp = true;
|
||
}
|
||
} else {
|
||
// Run outside an app dir with no --release flag. Use the latest
|
||
// release we know about (in the default track).
|
||
releaseName = await release.latestKnown();
|
||
if (!releaseName) {
|
||
// Somehow we have a catalog that doesn't have any releases on the
|
||
// default track. Try syncing, at least. (This is a pretty unlikely
|
||
// error case, since you should always start with a non-empty catalog.)
|
||
await Console.withProgressDisplayVisible(async function () {
|
||
await catalog.refreshOrWarn();
|
||
});
|
||
releaseName = await release.latestKnown();
|
||
}
|
||
if (!releaseName) {
|
||
if (catalog.refreshFailed) {
|
||
Console.error(
|
||
"The package catalog has no information about any Meteor",
|
||
"releases, and we had trouble connecting to the package server.");
|
||
} else {
|
||
Console.error(
|
||
"The package catalog has no information about",
|
||
"any Meteor releases.");
|
||
}
|
||
process.exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (releaseName !== undefined) {
|
||
// Yay, it's time to load releases!
|
||
//
|
||
// The release could be a modern (0.9.0+) tropohouse release or a legacy
|
||
// (pre-0.9.0) warehouse release.
|
||
//
|
||
// The release could be something we already know about on our local disk,
|
||
// or it could be something we have to ask a server about.
|
||
//
|
||
// We want to check both possibilities on disk before talking to any
|
||
// server. And we want to check for modern releases first in both cases.
|
||
|
||
var rel = null;
|
||
|
||
if (process.env.METEOR_TEST_FAIL_RELEASE_DOWNLOAD !== 'not-found') {
|
||
// ATTEMPT 1: modern release, on disk. (For modern releases, "on disk"
|
||
// just means we have the metadata about it in our catalog; it doesn't
|
||
// mean we've downloaded the tool or any packages yet.) release.load just
|
||
// does a single sqlite query; it doesn't refresh the catalog.
|
||
try {
|
||
rel = await release.load(releaseName);
|
||
} catch (e) {
|
||
if (!(e instanceof release.NoSuchReleaseError)) {
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
if (!rel) {
|
||
if (releaseName === null) {
|
||
throw Error("huh? couldn't load from-checkout release?");
|
||
}
|
||
|
||
// ATTEMPT 2: legacy release, on disk. (And it's a "real" release, not a
|
||
// "red pill" release which has the same name as a modern release!)
|
||
if (warehouse.realReleaseExistsInWarehouse(releaseName)) {
|
||
var manifest = await warehouse.ensureReleaseExistsAndReturnManifest(
|
||
releaseName);
|
||
await oldSpringboard(manifest.tools); // doesn't return
|
||
}
|
||
|
||
// ATTEMPT 3: modern release, troposphere sync needed.
|
||
await Console.withProgressDisplayVisible(async function () {
|
||
await catalog.refreshOrWarn();
|
||
});
|
||
|
||
// Try to load the release even if the refresh failed, since it might
|
||
// have failed on a later page than the one we needed.
|
||
try {
|
||
rel = await release.load(releaseName);
|
||
} catch (e) {
|
||
if (!(e instanceof release.NoSuchReleaseError)) {
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!rel && process.platform !== "win32") {
|
||
// ATTEMPT 4: legacy release, loading from warehouse server.
|
||
manifest = null;
|
||
try {
|
||
manifest = await warehouse.ensureReleaseExistsAndReturnManifest(
|
||
releaseName);
|
||
} catch (e) {
|
||
// Note: this is WAREHOUSE's NoSuchReleaseError, not RELEASE's
|
||
if (e instanceof warehouse.NoSuchReleaseError) {
|
||
// pass ...
|
||
} else if (e instanceof files.OfflineError) {
|
||
if (!catalog.refreshFailed) {
|
||
// Warn if we didn't already warn.
|
||
Console.warn(
|
||
"Unable to contact release server (are you offline?)");
|
||
Console.warn();
|
||
Console.warn(
|
||
"If you are using Meteor behind a proxy, set HTTP_PROXY and HTTPS_PROXY environment variables or see this page for more details: ",
|
||
Console.url("https://github.com/meteor/meteor/wiki/Using-Meteor-behind-a-proxy"));
|
||
}
|
||
// Treat this like a failure to refresh the catalog
|
||
// (map the old world to the new world)
|
||
catalog.refreshFailed = true;
|
||
} else {
|
||
throw e;
|
||
}
|
||
}
|
||
if (manifest) {
|
||
// OK, it was an legacy release. We should old-springboard to it.
|
||
await oldSpringboard(manifest.tools); // doesn't return
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!rel) {
|
||
// Nope, still have no idea about this release!
|
||
|
||
// Let's do some processing here. If the user/release file specified a
|
||
// track, we need to display that correctly, and if they didn't, we should
|
||
// make it clear that we are talking about the default track.
|
||
var catalogUtils = require('../packaging/catalog/catalog-utils.js');
|
||
var trackAndVersion = catalogUtils.splitReleaseName(releaseName);
|
||
var displayRelease = catalogUtils.displayRelease(
|
||
trackAndVersion[0], trackAndVersion[1]);
|
||
// Now, let's process this.
|
||
if (releaseOverride) {
|
||
if (process.platform === "win32") {
|
||
// Give a good warning if this release exists, but only in the super old
|
||
// warehouse.
|
||
var result = await httpHelpers.request(
|
||
"http://warehouse.meteor.com/releases/" + releaseName + ".release.json");
|
||
if(result.response.statusCode === 200) {
|
||
Console.error("Meteor on Windows does not support running any releases",
|
||
"before Meteor 1.1. Please use a newer release.");
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
Console.error(displayRelease + ": unknown release.");
|
||
} else if (appDir) {
|
||
if (trackAndVersion[0] !== catalog.DEFAULT_TRACK) {
|
||
displayRelease = "Meteor release " + displayRelease;
|
||
}
|
||
if (catalog.refreshFailed) {
|
||
Console.error(
|
||
"This project says that it uses " + displayRelease + ", but",
|
||
"you don't have that version of Meteor installed, and we were",
|
||
"unable to contact Meteor's update servers to find out about it.",
|
||
"Please edit the .meteor/release file in the project and change",
|
||
"it to a valid Meteor release, or go online.");
|
||
} else {
|
||
Console.error(
|
||
"This project says that it uses " + displayRelease + ", but you",
|
||
"don't have that version of Meteor installed and the Meteor",
|
||
"update servers don't have it either. Please edit the",
|
||
".meteor/release file in the project and change it to a valid",
|
||
"Meteor release.");
|
||
}
|
||
} else {
|
||
throw new Error("can't load latest release?");
|
||
}
|
||
process.exit(1);
|
||
}
|
||
|
||
release.setCurrent(rel, releaseForced, releaseExplicit);
|
||
}
|
||
|
||
// If we're not running the correct version of the tools for this
|
||
// release, fetch it and re-run.
|
||
//
|
||
// This will never happen when we're springboarding as part of an
|
||
// update, because the correct tools version will have been chosen
|
||
// the first time around. It will also never happen if the current
|
||
// release is a checkout, because that doesn't make any sense.
|
||
if (release.current &&
|
||
release.current.isProperRelease()) {
|
||
if (files.getToolsVersion() !==
|
||
release.current.getToolsPackageAtVersion()) {
|
||
await springboard(release.current, {
|
||
fromApp: releaseFromApp,
|
||
mayReturn: false,
|
||
});
|
||
// Does not return!
|
||
} else if (archinfo.canSwitchTo64Bit()) {
|
||
await springboard(release.current, {
|
||
fromApp: releaseFromApp,
|
||
// Switching to a 64-bit meteor-tool build may fail, in which case
|
||
// we should continue on as usual.
|
||
mayReturn: true,
|
||
});
|
||
}
|
||
}
|
||
|
||
try {
|
||
const { getChildProcess } = require("./dev-bundle-bin-commands")
|
||
|
||
const child = await getChildProcess({ isFirstTry: false })
|
||
|
||
// If we spawned a process to handle a dev_bundle/bin command like
|
||
// `meteor npm` or `meteor node`, then don't run any other tool code.
|
||
|
||
if (child) {
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
process.nextTick(function () {
|
||
throw error;
|
||
});
|
||
}
|
||
|
||
// Check for the '--help' option.
|
||
var showHelp = false;
|
||
if (_.has(rawOptions, '--help')) {
|
||
showHelp = true;
|
||
delete rawOptions['--help'];
|
||
}
|
||
|
||
var commandName = '';
|
||
var command = null;
|
||
|
||
// Check for a command like '--arch' or '--version'. Make sure
|
||
// it stands alone. (And this is ignored if you've passed --help.)
|
||
if (! showHelp) {
|
||
_.each(commands['--'] || {}, function (value, key) {
|
||
var fullName = "--" + key;
|
||
|
||
if (rawOptions[fullName]) {
|
||
if (rawOptions[fullName].length > 1) {
|
||
Console.error("It doesn't make sense to pass " +
|
||
fullName + " more than once.");
|
||
process.exit(1);
|
||
}
|
||
if (_.size(rawOptions) > 1 || rawArgs.length !== 0 || command) {
|
||
Console.error("Can't pass anything else along with " +
|
||
value.name + ".");
|
||
process.exit(1);
|
||
}
|
||
command = value;
|
||
commandName = command.name;
|
||
delete rawOptions['--' + key];
|
||
}
|
||
});
|
||
}
|
||
|
||
// OK, if not one of those, the first (non-'--') argument(s) should
|
||
// name the command.
|
||
if (! command) {
|
||
if (rawArgs.length === 0) {
|
||
// No arguments means 'run'. Unless it's 'meteor --help'.
|
||
if (! showHelp) {
|
||
command = commands.run
|
||
commandName = "run";
|
||
if (! command) {
|
||
throw new Error("no 'run' command?");
|
||
}
|
||
}
|
||
} else {
|
||
// Find the command they specified.
|
||
var walk = commands;
|
||
for (var i = 0; i < rawArgs.length; i++) {
|
||
var word = rawArgs[i];
|
||
|
||
// Support "meteor help", "meteor help deploy", "meteor help admin",
|
||
// "meteor admin help", "meteor admin help grant", etc. (But not
|
||
// "meteor deploy help" or "meteor admin grant help": once we find an
|
||
// actual command, we assume "help" is an argument, eg a site called
|
||
// 'help'!)
|
||
if (word === "help") {
|
||
showHelp = true;
|
||
continue;
|
||
}
|
||
|
||
commandName += (commandName.length > 0 ? " " : "") + word;
|
||
|
||
if (! _.has(walk, word)) {
|
||
Console.error(
|
||
Console.command("'" + commandName + "'") +
|
||
" is not a Meteor command. See " +
|
||
Console.command("'meteor --help'")+ ".");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (walk[word] instanceof Command) {
|
||
command = walk[word];
|
||
rawArgs = rawArgs.slice(i + 1); // consume arguments used
|
||
break;
|
||
}
|
||
|
||
walk = walk[word];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (! command && ! showHelp) {
|
||
// They typed something like 'meteor admin' (when they were
|
||
// supposed to type 'meteor admin grant' or something).
|
||
Console.error(
|
||
"Try " + Console.command("'meteor " + commandName + " help'") + " " +
|
||
"for available commands.");
|
||
process.exit(1);
|
||
}
|
||
|
||
// At this point we have a command[*]. Did they ask for help, or do
|
||
// they actually want to run the command? If the former, print the
|
||
// help and don't criticize anything else they may have given us.
|
||
//
|
||
// [*] the one exception being 'meteor --help' or 'meteor help', in
|
||
// which case showHelp will be true and command will be null
|
||
|
||
if (showHelp) {
|
||
// XXX: Until we rewrite the longHelp function to cope with the new output
|
||
// format, let's go with the static, painstakingly-formatted version.
|
||
Console.rawInfo(longHelp(commandName) + "\n");
|
||
process.exit(0);
|
||
}
|
||
|
||
// They want to run the command. Interpret the options and make sure
|
||
// that they're valid.
|
||
|
||
var options = { args: rawArgs };
|
||
|
||
_.each(command.options, function (optionInfo, optionName) {
|
||
var presentLong = _.has(rawOptions, "--" + optionName);
|
||
var presentShort = _.has(optionInfo, 'short') &&
|
||
_.has(rawOptions, "-" + optionInfo.short);
|
||
var tryHelpMessage =
|
||
"Try " + Console.command("'meteor help " + commandName + "'") + " " +
|
||
"for help.";
|
||
|
||
|
||
if (presentShort && presentLong) {
|
||
// this would get caught below, but give a clearer error message
|
||
Console.error(
|
||
commandName + ": can't pass both -" + optionInfo.short + " and --" +
|
||
optionName + ". " + tryHelpMessage);
|
||
process.exit(1);
|
||
}
|
||
var helpfulOptionName = "--" + optionName +
|
||
(presentShort ? " (-" + optionInfo.short + ")" : "");
|
||
|
||
// Collect all values we've received for this option, across the
|
||
// long and short versions, and across possibly multiple
|
||
// occurrences of the option on the command line
|
||
var values = [];
|
||
if (presentLong) {
|
||
values = values.concat(rawOptions["--" + optionName]);
|
||
}
|
||
if (presentShort) {
|
||
values = values.concat(rawOptions["-" + optionInfo.short]);
|
||
}
|
||
|
||
if (values.length > 1) {
|
||
// in the future, we could support multiple values, but we don't
|
||
// for now since no command needs it
|
||
Console.error(
|
||
Console.command(commandName) + ": can only take one " +
|
||
Console.command(helpfulOptionName) + " option.");
|
||
Console.error(tryHelpMessage);
|
||
|
||
process.exit(1);
|
||
} else if (values.length === 1) {
|
||
// OK, they provided exactly one value. Check its type and add
|
||
// to the output.
|
||
var value = values[0];
|
||
if (value === null) {
|
||
// This option requires a value and they didn't give it one
|
||
// (it was the last word on the command line).
|
||
Console.error(
|
||
Console.command(commandName) + ": the " +
|
||
Console.command(helpfulOptionName) + " option needs a value.");
|
||
Console.error(tryHelpMessage);
|
||
|
||
process.exit(1);
|
||
} else if (optionInfo.type === Number) {
|
||
if (! value.match(/^[0-9]+$/)) {
|
||
Console.error(
|
||
Console.command(commandName) + ": " +
|
||
Console.command(helpfulOptionName) + " must be a number.");
|
||
Console.error(tryHelpMessage);
|
||
process.exit(1);
|
||
}
|
||
value = parseInt(value);
|
||
} else if (optionInfo.type === Boolean) {
|
||
if (!value) {
|
||
Console.error(
|
||
Console.command(commandName) + ": the " +
|
||
Console.command(helpfulOptionName) + " " +
|
||
"option does not need a value.");
|
||
Console.error(tryHelpMessage);
|
||
process.exit(1);
|
||
}
|
||
value = true;
|
||
} else if (optionInfo.type === String) {
|
||
// nothing to do, 'value' needs no parsing or validation
|
||
} else {
|
||
throw new Error("unknown option type?");
|
||
}
|
||
options[optionName] = value;
|
||
|
||
// Remove from the list of input arguments so that later we can
|
||
// detect unrecognized arguments.
|
||
if (presentLong) {
|
||
delete rawOptions["--" + optionName];
|
||
}
|
||
if (presentShort) {
|
||
delete rawOptions["-" + optionInfo.short];
|
||
}
|
||
} else {
|
||
// Option not supplied. Throw an error if it was required,
|
||
// supply a default value if one is defined, or just leave it
|
||
// out.
|
||
if (_.has(optionInfo, 'default')) {
|
||
options[optionName] = optionInfo.default;
|
||
} else if (optionInfo.required) {
|
||
Console.error(
|
||
Console.command(commandName) + ": the --" +
|
||
Console.command(optionName) + " option is required.");
|
||
Console.rawError(longHelp(commandName));
|
||
process.exit(1);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Check for unrecognized options.
|
||
if (Object.keys(rawOptions).length > 0 && !command.allowUnrecognizedOptions) {
|
||
Console.error(
|
||
Console.command(Object.keys(rawOptions)[0]) + ": unknown option.");
|
||
Console.rawError(
|
||
longHelp(commandName));
|
||
process.exit(1);
|
||
}
|
||
|
||
// Check argument count.
|
||
if (options.args.length < command.minArgs) {
|
||
Console.error(
|
||
Console.command(commandName) + ": not enough arguments.");
|
||
Console.rawError(
|
||
longHelp(commandName));
|
||
process.exit(1);
|
||
}
|
||
|
||
if (options.args.length > command.maxArgs) {
|
||
Console.error(
|
||
Console.command(commandName) + ": too many arguments.");
|
||
Console.rawError(
|
||
longHelp(commandName));
|
||
process.exit(1);
|
||
}
|
||
|
||
// We know we have a valid command and options. Now check to see if
|
||
// the command can only be run from an app dir, and add the appDir
|
||
// option if running from an app.
|
||
var requiresApp = command.evaluateOption('requiresApp', options);
|
||
|
||
if (appDir) {
|
||
options.appDir = appDir;
|
||
}
|
||
|
||
if (requiresApp && ! options.appDir) {
|
||
// This is where you end up if you type 'meteor' with no args,
|
||
// since you'll default to the 'run' command which requires an
|
||
// app. Be welcoming to our new developers!
|
||
Console.error(
|
||
Console.command(commandName) +
|
||
": You're not in a Meteor project directory.");
|
||
Console.error();
|
||
Console.error("To create a new Meteor project:");
|
||
Console.error(
|
||
Console.command("meteor create <project name>"),
|
||
Console.options({ indent: 2 }));
|
||
Console.error("For example:");
|
||
Console.error(
|
||
Console.command("meteor create myapp"),
|
||
Console.options({ indent: 2 }));
|
||
Console.error();
|
||
Console.error(
|
||
"For more help, see " + Console.command("'meteor --help'") + ".");
|
||
process.exit(1);
|
||
}
|
||
|
||
// Same check for commands that can only be run from a package dir.
|
||
// You can't specify this on a Refresh.Never command.
|
||
var requiresPackage = command.evaluateOption('requiresPackage', options);
|
||
|
||
var requiresAppOrPackage =
|
||
command.evaluateOption('requiresAppOrPackage', options);
|
||
|
||
// Some commands have different results when run from a package dir, but don't
|
||
// strictly require it. These commands should use 'usesPackage' instead of
|
||
// requiresPackage. (We want to avoid searching up the directory tree for
|
||
// package.js when we don’t have to. Hopefully, a unified control file will
|
||
// allow us better control in the future).
|
||
var usesPackage = command.usesPackage;
|
||
|
||
if (requiresPackage || usesPackage || requiresAppOrPackage) {
|
||
var packageDir = files.findPackageDir();
|
||
if (packageDir) {
|
||
packageDir = files.pathResolve(packageDir);
|
||
}
|
||
if (packageDir) {
|
||
options.packageDir = packageDir;
|
||
}
|
||
}
|
||
|
||
if (requiresPackage && ! options.packageDir) {
|
||
Console.error(
|
||
Console.command(commandName) +
|
||
": You're not in a Meteor package directory.");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (requiresAppOrPackage && ! options.packageDir && ! options.appDir) {
|
||
Console.error(
|
||
Console.command(commandName) +
|
||
": You're not in a Meteor app directory or a Meteor package directory.");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (command.requiresRelease && ! release.current) {
|
||
Console.error(
|
||
"You must specify a Meteor version with --release when you work with",
|
||
"this project. It was created from an unreleased Meteor checkout and",
|
||
"doesn't have a version associated with it.");
|
||
Console.error(
|
||
"You can permanently set a release for this project with " +
|
||
Console.command("'meteor update'") + ".");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (command.requiresApp && release.current.isCheckout() &&
|
||
appReleaseFile && ! appReleaseFile.isCheckout()) {
|
||
// For commands that work with apps, if we have overridden the
|
||
// app's usual release by using a checkout, print a reminder banner.
|
||
Console.arrowWarn(
|
||
"Running Meteor from a checkout -- overrides project version " +
|
||
Console.noWrap("(" + appReleaseFile.displayReleaseName + ")"));
|
||
}
|
||
|
||
// Now that we're ready to start executing the command, if we are in
|
||
// startup time profiling mode, print the profile.
|
||
if (showRequireProfile) {
|
||
require('../tool-env/profile-require.js').printReport();
|
||
}
|
||
|
||
Console.setPretty(command.evaluateOption('pretty', options));
|
||
Console.enableProgressDisplay(true);
|
||
|
||
if (command.notOnWindows && process.platform === 'win32') {
|
||
Console.error('This command is not yet available on Windows.');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Run the command!
|
||
try {
|
||
// Before run, do a package sync if one is configured
|
||
var catalogRefreshStrategy = command.catalogRefresh;
|
||
if (! catalog.triedToRefreshRecently &&
|
||
catalogRefreshStrategy.beforeCommand) {
|
||
await buildmessage.enterJob({title: 'updating package catalog'}, async function () {
|
||
await catalogRefreshStrategy.beforeCommand();
|
||
});
|
||
}
|
||
|
||
var ret = await command.func(options, { rawOptions });
|
||
|
||
} catch (e) {
|
||
Console.enableProgressDisplay(false);
|
||
|
||
if (e === main.ShowUsage || e === main.WaitForExit ||
|
||
e === main.SpringboardToLatestRelease ||
|
||
e === main.SpringboardToSpecificRelease ||
|
||
e === main.WaitForExit) {
|
||
throw new Error(
|
||
"you meant 'throw new main.Foo', not 'throw main.Foo'");
|
||
} else if (e instanceof main.ShowUsage) {
|
||
Console.rawError(longHelp(commandName) + "\n");
|
||
process.exit(1);
|
||
} else if (e instanceof main.SpringboardToLatestRelease) {
|
||
// Load the metadata for the latest release (or at least, the latest
|
||
// release we know about locally). We should only do this if we know there
|
||
// is some latest release on this track.
|
||
var latestRelease = await release.load(await release.latestKnown(e.track));
|
||
await springboard(latestRelease, { releaseOverride: latestRelease.name });
|
||
// (does not return)
|
||
} else if (e instanceof main.SpringboardToSpecificRelease) {
|
||
// Springboard to a specific release. This is only throw by
|
||
// publish-for-arch, which is catalog.Refresh.OnceAtStart, so we ought to
|
||
// have decent knowledge of the latest release.
|
||
var nextRelease = await release.load(e.fullReleaseName);
|
||
await springboard(nextRelease, { releaseOverride: e.fullReleaseName });
|
||
// (does not return)
|
||
} else if (e instanceof main.WaitForExit) {
|
||
return;
|
||
} else if (e instanceof main.ExitWithCode) {
|
||
process.exit(e.code);
|
||
} else {
|
||
console.error(e);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
Console.enableProgressDisplay(false);
|
||
|
||
// Exit. (We will not get here if the command threw an exception
|
||
// such as main.WaitForExit).
|
||
if (ret === undefined) {
|
||
ret = 0;
|
||
}
|
||
if (typeof ret !== "number") {
|
||
throw new Error("command returned non-number?");
|
||
}
|
||
process.exit(ret);
|
||
});
|