start on a nicer greenlock cli
This commit is contained in:
parent
3ae0ecbc9d
commit
89dc5fe287
|
@ -0,0 +1,316 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var mkdirp = require('@root/mkdirp');
|
||||||
|
var cli = require('./cli.js');
|
||||||
|
|
||||||
|
cli.parse({
|
||||||
|
'directory-url': [
|
||||||
|
false,
|
||||||
|
' ACME Directory Resource URL',
|
||||||
|
'string',
|
||||||
|
'https://acme-v02.api.letsencrypt.org/directory',
|
||||||
|
'server,acme-url'
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
false,
|
||||||
|
' Email used for registration and recovery contact. (default: null)',
|
||||||
|
'email'
|
||||||
|
],
|
||||||
|
'agree-tos': [
|
||||||
|
false,
|
||||||
|
" Agree to the Greenlock and Let's Encrypt Subscriber Agreements",
|
||||||
|
'boolean',
|
||||||
|
false
|
||||||
|
],
|
||||||
|
'community-member': [
|
||||||
|
false,
|
||||||
|
' Submit stats to and get updates from Greenlock',
|
||||||
|
'boolean',
|
||||||
|
false
|
||||||
|
],
|
||||||
|
domains: [
|
||||||
|
false,
|
||||||
|
' Domain names to apply. For multiple domains you can enter a comma separated list of domains as a parameter. (default: [])',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
'renew-within': [
|
||||||
|
false,
|
||||||
|
' Renew certificates this many days before expiry',
|
||||||
|
'int',
|
||||||
|
7
|
||||||
|
],
|
||||||
|
'cert-path': [
|
||||||
|
false,
|
||||||
|
' Path to where new cert.pem is saved',
|
||||||
|
'string',
|
||||||
|
':configDir/live/:hostname/cert.pem'
|
||||||
|
],
|
||||||
|
'fullchain-path': [
|
||||||
|
false,
|
||||||
|
' Path to where new fullchain.pem (cert + chain) is saved',
|
||||||
|
'string',
|
||||||
|
':configDir/live/:hostname/fullchain.pem'
|
||||||
|
],
|
||||||
|
'bundle-path': [
|
||||||
|
false,
|
||||||
|
' Path to where new bundle.pem (fullchain + privkey) is saved',
|
||||||
|
'string',
|
||||||
|
':configDir/live/:hostname/bundle.pem'
|
||||||
|
],
|
||||||
|
'chain-path': [
|
||||||
|
false,
|
||||||
|
' Path to where new chain.pem is saved',
|
||||||
|
'string',
|
||||||
|
':configDir/live/:hostname/chain.pem'
|
||||||
|
],
|
||||||
|
'privkey-path': [
|
||||||
|
false,
|
||||||
|
' Path to where privkey.pem is saved',
|
||||||
|
'string',
|
||||||
|
':configDir/live/:hostname/privkey.pem'
|
||||||
|
],
|
||||||
|
'config-dir': [
|
||||||
|
false,
|
||||||
|
' Configuration directory.',
|
||||||
|
'string',
|
||||||
|
'~/letsencrypt/etc/'
|
||||||
|
],
|
||||||
|
store: [
|
||||||
|
false,
|
||||||
|
' The name of the storage module to use',
|
||||||
|
'string',
|
||||||
|
'greenlock-store-fs'
|
||||||
|
],
|
||||||
|
'store-xxxx': [
|
||||||
|
false,
|
||||||
|
' An option for the chosen storage module, such as --store-apikey or --store-bucket',
|
||||||
|
'bag'
|
||||||
|
],
|
||||||
|
'store-json': [
|
||||||
|
false,
|
||||||
|
' A JSON string containing all option for the chosen store module (instead of --store-xxxx)',
|
||||||
|
'json',
|
||||||
|
'{}'
|
||||||
|
],
|
||||||
|
challenge: [
|
||||||
|
false,
|
||||||
|
' The name of the HTTP-01, DNS-01, or TLS-ALPN-01 challenge module to use',
|
||||||
|
'string',
|
||||||
|
'@greenlock/acme-http-01-fs'
|
||||||
|
],
|
||||||
|
'challenge-xxxx': [
|
||||||
|
false,
|
||||||
|
' An option for the chosen challenge module, such as --challenge-apikey or --challenge-bucket',
|
||||||
|
'bag'
|
||||||
|
],
|
||||||
|
'challenge-json': [
|
||||||
|
false,
|
||||||
|
' A JSON string containing all option for the chosen challenge module (instead of --challenge-xxxx)',
|
||||||
|
'json',
|
||||||
|
'{}'
|
||||||
|
],
|
||||||
|
'skip-dry-run': [
|
||||||
|
false,
|
||||||
|
' Use with caution (and test with the staging url first). Creates an Order on the ACME server without a self-test.',
|
||||||
|
'boolean'
|
||||||
|
],
|
||||||
|
'skip-challenge-tests': [
|
||||||
|
false,
|
||||||
|
' Use with caution (and with the staging url first). Presents challenges to the ACME server without first testing locally.',
|
||||||
|
'boolean'
|
||||||
|
],
|
||||||
|
'http-01-port': [
|
||||||
|
false,
|
||||||
|
' Required to be 80 for live servers. Do not use. For special test environments only.',
|
||||||
|
'int'
|
||||||
|
],
|
||||||
|
'dns-01': [false, ' Use DNS-01 challange type', 'boolean', false],
|
||||||
|
standalone: [
|
||||||
|
false,
|
||||||
|
' Obtain certs using a "standalone" webserver.',
|
||||||
|
'boolean',
|
||||||
|
false
|
||||||
|
],
|
||||||
|
manual: [
|
||||||
|
false,
|
||||||
|
' Print the token and key to the screen and wait for you to hit enter, giving you time to copy it somewhere before continuing (uses acme-http-01-cli or acme-dns-01-cli)',
|
||||||
|
'boolean',
|
||||||
|
false
|
||||||
|
],
|
||||||
|
debug: [false, ' show traces and logs', 'boolean', false],
|
||||||
|
root: [
|
||||||
|
false,
|
||||||
|
' public_html / webroot path (may use the :hostname template such as /srv/www/:hostname)',
|
||||||
|
'string',
|
||||||
|
undefined,
|
||||||
|
'webroot-path'
|
||||||
|
],
|
||||||
|
|
||||||
|
//
|
||||||
|
// backwards compat
|
||||||
|
//
|
||||||
|
duplicate: [
|
||||||
|
false,
|
||||||
|
' Allow getting a certificate that duplicates an existing one/is an early renewal',
|
||||||
|
'boolean',
|
||||||
|
false
|
||||||
|
],
|
||||||
|
'rsa-key-size': [
|
||||||
|
false,
|
||||||
|
' (ignored) use domain-key-type or account-key-type instead',
|
||||||
|
'ignore',
|
||||||
|
2048
|
||||||
|
],
|
||||||
|
'domain-key-path': [
|
||||||
|
false,
|
||||||
|
' Path to privkey.pem to use for domain (default: generate new)',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
'domain-key-type': [
|
||||||
|
false,
|
||||||
|
" One of 'RSA' (2048), 'RSA-3084', 'RSA-4096', 'ECDSA' (P-256), or 'P-384'. For best compatibility, security, and efficiency use the default (More bits != More security)",
|
||||||
|
'string',
|
||||||
|
'RSA'
|
||||||
|
],
|
||||||
|
'account-key-path': [
|
||||||
|
false,
|
||||||
|
' Path to privkey.pem to use for account (default: generate new)',
|
||||||
|
'string'
|
||||||
|
],
|
||||||
|
'account-key-type': [
|
||||||
|
false,
|
||||||
|
" One of 'ECDSA' (P-256), 'P-384', 'RSA', 'RSA-3084', or 'RSA-4096'. Stick with 'ECDSA' (P-256) unless you need 'RSA' (2048) for legacy compatibility. (More bits != More security)",
|
||||||
|
'string',
|
||||||
|
'P-256'
|
||||||
|
],
|
||||||
|
webroot: [false, ' (ignored) for certbot compatibility', 'ignore', false],
|
||||||
|
//, 'standalone-supported-challenges': [ false, " Supported challenges, order preferences are randomly chosen. (default: http-01,tls-sni-01)", 'string', 'http-01,tls-sni-01']
|
||||||
|
'work-dir': [
|
||||||
|
false,
|
||||||
|
' for certbot compatibility (ignored)',
|
||||||
|
'string',
|
||||||
|
'~/letsencrypt/var/lib/'
|
||||||
|
],
|
||||||
|
'logs-dir': [
|
||||||
|
false,
|
||||||
|
' for certbot compatibility (ignored)',
|
||||||
|
'string',
|
||||||
|
'~/letsencrypt/var/log/'
|
||||||
|
],
|
||||||
|
'acme-version': [
|
||||||
|
false,
|
||||||
|
' (ignored) ACME is now RFC 8555 and prior drafts are no longer supported',
|
||||||
|
'ignore',
|
||||||
|
'rfc8555'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// ignore certonly and extraneous arguments
|
||||||
|
cli.main(function(_, options) {
|
||||||
|
console.info('');
|
||||||
|
|
||||||
|
[
|
||||||
|
'configDir',
|
||||||
|
'privkeyPath',
|
||||||
|
'certPath',
|
||||||
|
'chainPath',
|
||||||
|
'fullchainPath',
|
||||||
|
'bundlePath'
|
||||||
|
].forEach(function(k) {
|
||||||
|
if (options[k]) {
|
||||||
|
options.storeOpts[k] = options[k];
|
||||||
|
}
|
||||||
|
delete options[k];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.workDir) {
|
||||||
|
options.challengeOpts.workDir = options.workDir;
|
||||||
|
delete options.workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.debug) {
|
||||||
|
console.debug(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = {};
|
||||||
|
var homedir = require('os').homedir();
|
||||||
|
|
||||||
|
Object.keys(options).forEach(function(key) {
|
||||||
|
var val = options[key];
|
||||||
|
|
||||||
|
if ('string' === typeof val) {
|
||||||
|
val = val.replace(/^~/, homedir);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = key.replace(/\-([a-z0-9A-Z])/g, function(c) {
|
||||||
|
return c[1].toUpperCase();
|
||||||
|
});
|
||||||
|
args[key] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(args).forEach(function(key) {
|
||||||
|
var val = args[key];
|
||||||
|
|
||||||
|
if ('string' === typeof val) {
|
||||||
|
val = val.replace(/(\:configDir)|(\:config)/, args.configDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
args[key] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args.domains) {
|
||||||
|
args.domains = args.domains.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(Array.isArray(args.domains) && args.domains.length) ||
|
||||||
|
!args.email ||
|
||||||
|
!args.agreeTos ||
|
||||||
|
(!args.server && !args.directoryUrl)
|
||||||
|
) {
|
||||||
|
console.error('\nUsage:\n\ngreenlock certonly --standalone \\');
|
||||||
|
console.error(
|
||||||
|
'\t--agree-tos --email user@example.com --domains example.com \\'
|
||||||
|
);
|
||||||
|
console.error('\t--config-dir ~/acme/etc \\');
|
||||||
|
console.error('\nSee greenlock --help for more details\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.http01Port) {
|
||||||
|
// [@agnat]: Coerce to string. cli returns a number although we request a string.
|
||||||
|
args.http01Port = '' + args.http01Port;
|
||||||
|
args.http01Port = args.http01Port.split(',').map(function(port) {
|
||||||
|
return parseInt(port, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
console.log('\ngot to the run step');
|
||||||
|
process.exit(1);
|
||||||
|
require('../')
|
||||||
|
.run(args)
|
||||||
|
.then(function(status) {
|
||||||
|
process.exit(status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('greenlock-store-fs' !== args.store) {
|
||||||
|
run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove mkdirp and let greenlock-store-fs do this?
|
||||||
|
mkdirp(args.storeOpts.configDir, function(err) {
|
||||||
|
if (!err) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"Could not create --config-dir '" + args.configDir + "':",
|
||||||
|
err.code
|
||||||
|
);
|
||||||
|
console.error("Try setting --config-dir '/tmp'");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}, process.argv.slice(3));
|
|
@ -0,0 +1,234 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var CLI = module.exports;
|
||||||
|
|
||||||
|
var defaultConf;
|
||||||
|
var defaultOpts;
|
||||||
|
var bags = [];
|
||||||
|
|
||||||
|
CLI.parse = function(conf) {
|
||||||
|
var opts = (defaultOpts = {});
|
||||||
|
defaultConf = conf;
|
||||||
|
|
||||||
|
Object.keys(conf).forEach(function(k) {
|
||||||
|
var v = conf[k];
|
||||||
|
var aliases = v[5];
|
||||||
|
var bag;
|
||||||
|
var bagName;
|
||||||
|
|
||||||
|
// the name of the argument set is now the 0th argument
|
||||||
|
v.unshift(k);
|
||||||
|
// v[0] flagname
|
||||||
|
// v[1] short flagname
|
||||||
|
// v[2] description
|
||||||
|
// v[3] type
|
||||||
|
// v[4] default value
|
||||||
|
// v[5] aliases
|
||||||
|
|
||||||
|
if ('bag' === v[3]) {
|
||||||
|
bag = v[0]; // 'bag-option-xxxx' => '--bag-option-'
|
||||||
|
bag = '--' + bag.replace(/xxx.*/, '');
|
||||||
|
bags.push(bag);
|
||||||
|
|
||||||
|
bagName = toBagName(bag.replace(/^--/, ''));
|
||||||
|
opts[bagName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('json' === v[3]) {
|
||||||
|
bagName = toBagName(v[0].replace(/-json$/, '')); // 'bag-option-json' => 'bagOptionOpts'
|
||||||
|
opts[bagName] = {};
|
||||||
|
} else if ('ignore' !== v[3] && 'undefined' !== typeof v[4]) {
|
||||||
|
// set the default values (where 'undefined' is not an allowed value)
|
||||||
|
opts[toCamel(k)] = v[4];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!aliases) {
|
||||||
|
aliases = [];
|
||||||
|
} else if ('string' === typeof aliases) {
|
||||||
|
aliases = aliases.split(',');
|
||||||
|
}
|
||||||
|
aliases.forEach(function(alias) {
|
||||||
|
if (alias in conf) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot alias '" +
|
||||||
|
alias +
|
||||||
|
"' from '" +
|
||||||
|
k +
|
||||||
|
"': option already exists"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
conf[alias] = v;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
CLI.main = function(cb, args) {
|
||||||
|
var leftovers = [];
|
||||||
|
var conf = defaultConf;
|
||||||
|
var opts = defaultOpts;
|
||||||
|
|
||||||
|
if (!opts) {
|
||||||
|
throw new Error("you didn't call `CLI.parse(configuration)`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO what's the existing API for this?
|
||||||
|
if (!args) {
|
||||||
|
args = process.argv.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var flag;
|
||||||
|
var cnf;
|
||||||
|
var typ;
|
||||||
|
|
||||||
|
function grab(bag) {
|
||||||
|
var bagName = toBagName(bag);
|
||||||
|
if (bag !== flag.slice(0, bag.length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(bagName, toCamel(flag.slice(bag.length)));
|
||||||
|
opts[bagName][toCamel(flag.slice(bag.length))] = args.shift();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (args.length) {
|
||||||
|
// take one off the top
|
||||||
|
flag = args.shift();
|
||||||
|
|
||||||
|
// mind the gap
|
||||||
|
if ('--' === flag) {
|
||||||
|
leftovers = leftovers.concat(args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// help!
|
||||||
|
if (
|
||||||
|
'--help' === flag ||
|
||||||
|
'-h' === flag ||
|
||||||
|
'/?' === flag ||
|
||||||
|
'help' === flag
|
||||||
|
) {
|
||||||
|
printHelp(conf);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only long names are actually used
|
||||||
|
if ('--' !== flag.slice(0, 2)) {
|
||||||
|
console.error("Unrecognized argument '" + flag + "'");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cnf = conf[flag.slice(2)];
|
||||||
|
if (!cnf) {
|
||||||
|
// look for arbitrary flags
|
||||||
|
if (bags.some(grab)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other arbitrary args are not used
|
||||||
|
console.error("Unrecognized flag '" + flag + "'");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// encourage switching to non-aliased version
|
||||||
|
if (flag !== '--' + cnf[0]) {
|
||||||
|
console.warn(
|
||||||
|
"use of '" +
|
||||||
|
flag +
|
||||||
|
"' is deprecated, use '--" +
|
||||||
|
cnf[0] +
|
||||||
|
"' instead"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for xxx-json flags
|
||||||
|
if ('json' === cnf[3]) {
|
||||||
|
try {
|
||||||
|
var json = JSON.parse(args.shift());
|
||||||
|
var bagName = toBagName(cnf[0].replace(/-json$/, ''));
|
||||||
|
Object.keys(json).forEach(function(k) {
|
||||||
|
opts[bagName][k] = json[k];
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse option '" + flag + "' as JSON:");
|
||||||
|
console.error(e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set booleans, otherwise grab the next arg in line
|
||||||
|
typ = cnf[3];
|
||||||
|
// TODO --no-<whatever> to negate
|
||||||
|
if (Boolean === typ || 'boolean' === typ) {
|
||||||
|
opts[toCamel(cnf[0])] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
opts[toCamel(cnf[0])] = args.shift();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(leftovers, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
function toCamel(str) {
|
||||||
|
return str.replace(/-([a-z0-9])/g, function(m) {
|
||||||
|
return m[1].toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBagName(bag) {
|
||||||
|
// trim leading and trailing '-'
|
||||||
|
bag = bag.replace(/^-+/g, '').replace(/-+$/g, '')
|
||||||
|
return toCamel(bag) + 'Opts'; // '--bag-option-' => bagOptionOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp(conf) {
|
||||||
|
var flagLen = 0;
|
||||||
|
var typeLen = 0;
|
||||||
|
var defLen = 0;
|
||||||
|
|
||||||
|
Object.keys(conf).forEach(function(k) {
|
||||||
|
flagLen = Math.max(flagLen, conf[k][0].length);
|
||||||
|
typeLen = Math.max(typeLen, conf[k][3].length);
|
||||||
|
if ('undefined' !== typeof conf[k][4]) {
|
||||||
|
defLen = Math.max(
|
||||||
|
defLen,
|
||||||
|
'(Default: )'.length + String(conf[k][4]).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(conf).forEach(function(k) {
|
||||||
|
var v = conf[k];
|
||||||
|
|
||||||
|
// skip aliases
|
||||||
|
if (v[0] !== k) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var def = v[4];
|
||||||
|
if ('undefined' === typeof def) {
|
||||||
|
def = '';
|
||||||
|
} else {
|
||||||
|
def = '(default: ' + JSON.stringify(def) + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg =
|
||||||
|
' --' +
|
||||||
|
v[0].padEnd(flagLen) +
|
||||||
|
' ' +
|
||||||
|
v[3].padStart(typeLen + 1) +
|
||||||
|
' ' +
|
||||||
|
(v[2] || '') +
|
||||||
|
' ' +
|
||||||
|
def; /*.padStart(defLen)*/
|
||||||
|
// v[0] flagname
|
||||||
|
// v[1] short flagname
|
||||||
|
// v[2] description
|
||||||
|
// v[3] type
|
||||||
|
// v[4] default value
|
||||||
|
// v[5] aliases
|
||||||
|
|
||||||
|
console.info(msg);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var args = process.argv.slice(2);
|
||||||
|
console.log(args);
|
||||||
|
if ('certonly' === args[0]) {
|
||||||
|
require('./certonly.js');
|
||||||
|
return;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var spawn = require('child_process').spawn;
|
||||||
|
var path = require('path');
|
||||||
|
var PKG_DIR = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
module.exports.install = function(moduleName) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
if (!moduleName) {
|
||||||
|
throw new Error('no module name given');
|
||||||
|
}
|
||||||
|
|
||||||
|
var npm = 'npm';
|
||||||
|
var args = ['install', '--save', moduleName];
|
||||||
|
var out = '';
|
||||||
|
var cmd = spawn(npm, args, {
|
||||||
|
cwd: PKG_DIR,
|
||||||
|
windowsHide: true
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.stdout.on('data', function(chunk) {
|
||||||
|
out += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
cmd.stdout.on('data', function(chunk) {
|
||||||
|
out += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('error', function(e) {
|
||||||
|
console.error(
|
||||||
|
"Failed to start: '" +
|
||||||
|
npm +
|
||||||
|
' ' +
|
||||||
|
args.join(' ') +
|
||||||
|
"' in '" +
|
||||||
|
PKG_DIR +
|
||||||
|
"'"
|
||||||
|
);
|
||||||
|
console.error(e.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('exit', function(code) {
|
||||||
|
if (!code) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out) {
|
||||||
|
console.error(out);
|
||||||
|
console.error();
|
||||||
|
console.error();
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
"Failed to run: '" +
|
||||||
|
npm +
|
||||||
|
' ' +
|
||||||
|
args.join(' ') +
|
||||||
|
"' in '" +
|
||||||
|
PKG_DIR +
|
||||||
|
"'"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'Try for yourself:\n\tcd ' +
|
||||||
|
PKG_DIR +
|
||||||
|
'\n\tnpm ' +
|
||||||
|
args.join(' ')
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
module.exports.install(process.argv[2]);
|
||||||
|
}
|
Loading…
Reference in New Issue