CLI refactor with commander (#6058)

* CLI refactor with commander

- Modularized the functionality
- All functionality done directly from `./nodebb` now
(still available from `app` for backwards compatibility)
- Moved all CLI code from `./nodebb` to `src/cli`
- Fixed `nodebb.bat` to work from any location, like `./nodebb`, and
also hides command output
- Overwrite some commander methods to add CLI color support
- Added `./nodebb info` for quick info including git hash, NodeBB
version, node version, and some database info
- Refactored `./nodebb reset` to allow multiple resets at once
- Changed `./nodebb restart` to essentially stop and start, as Windows
doesn't support signals
- Added `-l, --log` option which works on `./nodebb start` and `./nodebb
restart` to show logging, like `./nodebb slog`
- Expanded `-d, --dev` option which works on them as well, like
`./nodebb dev`
- Improvements to self-help. `./nodebb build -h` will output all
possible targets
- `./nodebb reset` explains usage better

* Fix some style inconsistencies

* Fix prestart being required before modules installed

* Fix travis failures

* Fix `help` command to output help for subcommands

* Pick steps of the upgrade process to run

* Fix formatting for upgrade help

* Fix web installer
v1.18.x
Peter Jaszkowiak 7 years ago committed by Barış Soner Uşaklı
parent c731661a39
commit ae24bca16e

266
app.js

@ -30,47 +30,16 @@ nconf.argv().env({
separator: '__', separator: '__',
}); });
var url = require('url');
var async = require('async'); var async = require('async');
var winston = require('winston'); var winston = require('winston');
var path = require('path'); var path = require('path');
var pkg = require('./package.json');
var file = require('./src/file'); var file = require('./src/file');
var debug = require('./src/meta/debugFork').debugging;
global.env = process.env.NODE_ENV || 'production'; global.env = process.env.NODE_ENV || 'production';
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
colorize: true,
timestamp: function () {
var date = new Date();
return nconf.get('json-logging') ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0, 8) + ' [' + global.process.pid + ']';
},
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'),
json: (!!nconf.get('json-logging')),
stringify: (!!nconf.get('json-logging')),
});
if (debug) {
var winstonCommon = require('winston/lib/winston/common');
// Override to use real console.log etc for VSCode debugger
winston.transports.Console.prototype.log = function (level, message, meta, callback) {
const output = winstonCommon.log(Object.assign({}, this, {
level,
message,
meta,
}));
console[level in console ? level : 'log'](output);
setImmediate(callback, null, true);
};
}
// Alternate configuration file support // Alternate configuration file support
var configFile = path.join(__dirname, '/config.json'); var configFile = path.join(__dirname, 'config.json');
if (nconf.get('config')) { if (nconf.get('config')) {
configFile = path.resolve(__dirname, nconf.get('config')); configFile = path.resolve(__dirname, nconf.get('config'));
@ -78,8 +47,9 @@ if (nconf.get('config')) {
var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database'));
loadConfig(); var prestart = require('./src/prestart');
versionCheck(); prestart.loadConfig(configFile);
prestart.versionCheck();
if (!process.send) { if (!process.send) {
// If run using `node app`, log GNU copyright info along with server info // If run using `node app`, log GNU copyright info along with server info
@ -89,224 +59,40 @@ if (!process.send) {
winston.info(''); winston.info('');
} }
if (nconf.get('setup') || nconf.get('install')) { if (nconf.get('setup') || nconf.get('install')) {
setup(); require('./src/cli/setup').setup();
} else if (!configExists) { } else if (!configExists) {
require('./install/web').install(nconf.get('port')); require('./install/web').install(nconf.get('port'));
} else if (nconf.get('upgrade')) { } else if (nconf.get('upgrade')) {
upgrade(); require('./src/cli/upgrade').upgrade(true);
} else if (nconf.get('reset')) { } else if (nconf.get('reset')) {
async.waterfall([ var options = {
async.apply(require('./src/reset').reset), theme: nconf.get('t'),
async.apply(require('./src/meta/build').buildAll), plugin: nconf.get('p'),
widgets: nconf.get('w'),
settings: nconf.get('s'),
all: nconf.get('a'),
};
async.series([
async.apply(require('./src/cli/reset').reset, options),
require('./src/meta/build').buildAll,
], function (err) { ], function (err) {
process.exit(err ? 1 : 0); if (err) {
throw err;
}
process.exit(0);
}); });
} else if (nconf.get('activate')) { } else if (nconf.get('activate')) {
activate(); require('./src/cli/manage').activate(nconf.get('activate'));
} else if (nconf.get('plugins')) { } else if (nconf.get('plugins')) {
listPlugins(); require('./src/cli/manage').listPlugins();
} else if (nconf.get('build')) { } else if (nconf.get('build')) {
require('./src/meta/build').build(nconf.get('build')); require('./src/meta/build').build(nconf.get('build'));
} else if (nconf.get('events')) { } else if (nconf.get('events')) {
async.series([ require('./src/cli/manage').listEvents();
async.apply(require('./src/database').init),
async.apply(require('./src/events').output),
]);
} else { } else {
require('./src/start').start(); require('./src/start').start();
} }
function loadConfig(callback) {
winston.verbose('* using configuration stored in: %s', configFile);
nconf.file({
file: configFile,
});
nconf.defaults({
base_dir: __dirname,
themes_path: path.join(__dirname, 'node_modules'),
upload_path: 'public/uploads',
views_dir: path.join(__dirname, 'build/public/templates'),
version: pkg.version,
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
// Ensure themes_path is a full filepath
nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path')));
nconf.set('core_templates_path', path.join(__dirname, 'src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
nconf.set('upload_path', path.resolve(nconf.get('base_dir'), nconf.get('upload_path')));
if (nconf.get('url')) {
nconf.set('url_parsed', url.parse(nconf.get('url')));
}
// Explicitly cast 'jobsDisabled' as Bool
var castAsBool = ['jobsDisabled'];
nconf.stores.env.readOnly = false;
castAsBool.forEach(function (prop) {
var value = nconf.get(prop);
if (value) {
nconf.set(prop, typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true');
}
});
nconf.stores.env.readOnly = true;
if (typeof callback === 'function') {
callback();
}
}
function setup() {
winston.info('NodeBB Setup Triggered via Command Line');
var install = require('./src/install');
var build = require('./src/meta/build');
process.stdout.write('\nWelcome to NodeBB!\n');
process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n');
process.stdout.write('Press enter to accept the default setting (shown in brackets).\n');
async.series([
async.apply(install.setup),
async.apply(loadConfig),
async.apply(build.buildAll),
], function (err, data) {
// Disregard build step data
data = data[0];
var separator = ' ';
if (process.stdout.columns > 10) {
for (var x = 0, cols = process.stdout.columns - 10; x < cols; x += 1) {
separator += '=';
}
}
process.stdout.write('\n' + separator + '\n\n');
if (err) {
winston.error('There was a problem completing NodeBB setup', err);
throw err;
} else {
if (data.hasOwnProperty('password')) {
process.stdout.write('An administrative user was automatically created for you:\n');
process.stdout.write(' Username: ' + data.username + '\n');
process.stdout.write(' Password: ' + data.password + '\n');
process.stdout.write('\n');
}
process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups)
if (process.send) {
process.send(data);
}
}
process.exit();
});
}
function upgrade() {
var db = require('./src/database');
var meta = require('./src/meta');
var upgrade = require('./src/upgrade');
var build = require('./src/meta/build');
var tasks = [db.init, meta.configs.init];
if (nconf.get('upgrade') !== true) {
// Likely an upgrade script name passed in
tasks.push(async.apply(upgrade.runParticular, nconf.get('upgrade').split(',')));
} else {
tasks.push(upgrade.run, build.buildAll);
}
// disable mongo timeouts during upgrade
nconf.set('mongo:options:socketTimeoutMS', 0);
async.series(tasks, function (err) {
if (err) {
winston.error(err.stack);
process.exit(1);
} else {
process.exit(0);
}
});
}
function activate() {
var db = require('./src/database');
var plugins = require('./src/plugins');
var events = require('./src/events');
var plugin = nconf.get('activate');
async.waterfall([
function (next) {
db.init(next);
},
function (next) {
if (plugin.indexOf('nodebb-') !== 0) {
// Allow omission of `nodebb-plugin-`
plugin = 'nodebb-plugin-' + plugin;
}
plugins.isInstalled(plugin, next);
},
function (isInstalled, next) {
if (!isInstalled) {
return next(new Error('plugin not installed'));
}
winston.info('Activating plugin `%s`', plugin);
db.sortedSetAdd('plugins:active', 0, plugin, next);
},
function (next) {
events.log({
type: 'plugin-activate',
text: plugin,
}, next);
},
], function (err) {
if (err) {
winston.error('An error occurred during plugin activation', err);
throw err;
}
process.exit(0);
});
}
function listPlugins() {
require('./src/database').init(function (err) {
if (err) {
winston.error(err.stack);
process.exit(1);
}
var db = require('./src/database');
db.getSortedSetRange('plugins:active', 0, -1, function (err, plugins) {
if (err) {
winston.error(err.stack);
process.exit(1);
}
winston.info('Active plugins: \n\t - ' + plugins.join('\n\t - '));
process.exit();
});
});
}
function versionCheck() {
var version = process.version.slice(1);
var range = pkg.engines.node;
var semver = require('semver');
var compatible = semver.satisfies(version, range);
if (!compatible) {
winston.warn('Your version of Node.js is too outdated for NodeBB. Please update your version of Node.js.');
winston.warn('Recommended ' + range.green + ', '.reset + version.yellow + ' provided\n'.reset);
}
}

@ -27,6 +27,7 @@
"chart.js": "^2.7.0", "chart.js": "^2.7.0",
"colors": "^1.1.2", "colors": "^1.1.2",
"compression": "^1.7.1", "compression": "^1.7.1",
"commander": "^2.11.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"connect-mongo": "2.0.0", "connect-mongo": "2.0.0",
@ -52,7 +53,6 @@
"logrotate-stream": "^0.2.5", "logrotate-stream": "^0.2.5",
"lru-cache": "4.1.1", "lru-cache": "4.1.1",
"mime": "^2.0.3", "mime": "^2.0.3",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mongodb": "2.2.33", "mongodb": "2.2.33",
"morgan": "^1.9.0", "morgan": "^1.9.0",

547
nodebb

@ -2,549 +2,4 @@
'use strict'; 'use strict';
var fs = require('fs'); require('./src/cli');
var path = require('path');
var cproc = require('child_process');
var packageInstall = require('./src/meta/package-install');
// check to make sure dependencies are installed
try {
fs.readFileSync(path.join(__dirname, './package.json'));
fs.readFileSync(path.join(__dirname, 'node_modules/async/package.json'));
} catch (e) {
if (e.code === 'ENOENT') {
process.stdout.write('Dependencies not yet installed.\n');
process.stdout.write('Installing them now...\n\n');
packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins();
packageInstall.npmInstallProduction();
} else {
throw e;
}
}
var minimist;
var request;
var semver;
var prompt;
var async;
try {
require('colors');
minimist = require('minimist');
request = require('request');
semver = require('semver');
prompt = require('prompt');
async = require('async');
} catch (e) {
process.stdout.write(
'\x1b[31mNodeBB could not be initialised because there was an error while loading dependencies.\n' +
'Please run "\x1b[33mnpm install --production\x1b[31m" and try again.\x1b[0m\n\n' +
'For more information, please see: https://docs.nodebb.org/installing/os/\n\n'
);
throw e;
}
var args = minimist(process.argv.slice(2));
var loaderPath = path.join(__dirname, 'loader.js');
var appPath = path.join(__dirname, 'app.js');
if (args.dev) {
process.env.NODE_ENV = 'development';
}
function getRunningPid(callback) {
fs.readFile(path.join(__dirname, 'pidfile'), {
encoding: 'utf-8',
}, function (err, pid) {
if (err) {
return callback(err);
}
try {
process.kill(parseInt(pid, 10), 0);
callback(null, parseInt(pid, 10));
} catch (e) {
callback(e);
}
});
}
function getCurrentVersion(callback) {
fs.readFile(path.join(__dirname, 'package.json'), { encoding: 'utf-8' }, function (err, pkg) {
if (err) {
return callback(err);
}
try {
pkg = JSON.parse(pkg);
return callback(null, pkg.version);
} catch (err) {
return callback(err);
}
});
}
function fork(args) {
return cproc.fork(appPath, args, {
cwd: __dirname,
silent: false,
});
}
function getInstalledPlugins(callback) {
async.parallel({
files: async.apply(fs.readdir, path.join(__dirname, 'node_modules')),
deps: async.apply(fs.readFile, path.join(__dirname, 'package.json'), { encoding: 'utf-8' }),
}, function (err, payload) {
if (err) {
return callback(err);
}
var isNbbModule = /^nodebb-(?:plugin|theme|widget|rewards)-[\w-]+$/;
var moduleName;
var isGitRepo;
payload.files = payload.files.filter(function (file) {
return isNbbModule.test(file);
});
try {
payload.deps = JSON.parse(payload.deps).dependencies;
payload.bundled = [];
payload.installed = [];
} catch (err) {
return callback(err);
}
for (moduleName in payload.deps) {
if (isNbbModule.test(moduleName)) {
payload.bundled.push(moduleName);
}
}
// Whittle down deps to send back only extraneously installed plugins/themes/etc
payload.files.forEach(function (moduleName) {
try {
fs.accessSync(path.join(__dirname, 'node_modules/' + moduleName, '.git'));
isGitRepo = true;
} catch (e) {
isGitRepo = false;
}
if (
payload.files.indexOf(moduleName) !== -1 && // found in `node_modules/`
payload.bundled.indexOf(moduleName) === -1 && // not found in `package.json`
!fs.lstatSync(path.join(__dirname, 'node_modules/' + moduleName)).isSymbolicLink() && // is not a symlink
!isGitRepo // .git/ does not exist, so it is not a git repository
) {
payload.installed.push(moduleName);
}
});
getModuleVersions(payload.installed, callback);
});
}
function getModuleVersions(modules, callback) {
var versionHash = {};
async.eachLimit(modules, 50, function (module, next) {
fs.readFile(path.join(__dirname, 'node_modules/' + module + '/package.json'), { encoding: 'utf-8' }, function (err, pkg) {
if (err) {
return next(err);
}
try {
pkg = JSON.parse(pkg);
versionHash[module] = pkg.version;
next();
} catch (err) {
next(err);
}
});
}, function (err) {
callback(err, versionHash);
});
}
function checkPlugins(standalone, callback) {
if (standalone) {
process.stdout.write('Checking installed plugins and themes for updates... ');
}
async.waterfall([
async.apply(async.parallel, {
plugins: async.apply(getInstalledPlugins),
version: async.apply(getCurrentVersion),
}),
function (payload, next) {
var toCheck = Object.keys(payload.plugins);
if (!toCheck.length) {
process.stdout.write('OK'.green + '\n'.reset);
return next(null, []); // no extraneous plugins installed
}
request({
method: 'GET',
url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='),
json: true,
}, function (err, res, body) {
if (err) {
process.stdout.write('error'.red + '\n'.reset);
return next(err);
}
process.stdout.write('OK'.green + '\n'.reset);
if (!Array.isArray(body) && toCheck.length === 1) {
body = [body];
}
var current;
var suggested;
var upgradable = body.map(function (suggestObj) {
current = payload.plugins[suggestObj.package];
suggested = suggestObj.version;
if (suggestObj.code === 'match-found' && semver.gt(suggested, current)) {
return {
name: suggestObj.package,
current: current,
suggested: suggested,
};
}
return null;
}).filter(Boolean);
next(null, upgradable);
});
},
], callback);
}
function upgradePlugins(callback) {
var standalone = false;
if (typeof callback !== 'function') {
callback = function () {};
standalone = true;
}
checkPlugins(standalone, function (err, found) {
if (err) {
process.stdout.write('Warning'.yellow + ': An unexpected error occured when attempting to verify plugin upgradability\n'.reset);
return callback(err);
}
if (found && found.length) {
process.stdout.write('\nA total of ' + String(found.length).bold + ' package(s) can be upgraded:\n');
found.forEach(function (suggestObj) {
process.stdout.write(' * '.yellow + suggestObj.name.reset + ' (' + suggestObj.current.yellow + ' -> '.reset + suggestObj.suggested.green + ')\n'.reset);
});
process.stdout.write('\n');
} else {
if (standalone) {
process.stdout.write('\nAll packages up-to-date!'.green + '\n'.reset);
}
return callback();
}
prompt.message = '';
prompt.delimiter = '';
prompt.start();
prompt.get({
name: 'upgrade',
description: 'Proceed with upgrade (y|n)?'.reset,
type: 'string',
}, function (err, result) {
if (err) {
return callback(err);
}
if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) {
process.stdout.write('\nUpgrading packages...');
var args = ['i'];
found.forEach(function (suggestObj) {
args.push(suggestObj.name + '@' + suggestObj.suggested);
});
cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', args, { stdio: 'ignore' }, function (err) {
if (!err) {
process.stdout.write(' OK\n'.green);
}
callback(err);
});
} else {
process.stdout.write('\nPackage upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade-plugins'.green + '".\n'.reset);
callback();
}
});
});
}
var commands = {
status: {
description: 'View the status of the NodeBB server',
usage: 'Usage: ' + './nodebb status'.yellow,
handler: function () {
getRunningPid(function (err, pid) {
if (!err) {
process.stdout.write('\nNodeBB Running '.bold + '(pid '.cyan + pid.toString().cyan + ')\n'.cyan);
process.stdout.write('\t"' + './nodebb stop'.yellow + '" to stop the NodeBB server\n');
process.stdout.write('\t"' + './nodebb log'.yellow + '" to view server output\n');
process.stdout.write('\t"' + './nodebb restart'.yellow + '" to restart NodeBB\n\n');
} else {
process.stdout.write('\nNodeBB is not running\n'.bold);
process.stdout.write('\t"' + './nodebb start'.yellow + '" to launch the NodeBB server\n\n'.reset);
}
});
},
},
start: {
description: 'Start the NodeBB server',
usage: 'Usage: ' + './nodebb start'.yellow,
handler: function () {
process.stdout.write('\nStarting NodeBB\n'.bold);
process.stdout.write(' "' + './nodebb stop'.yellow + '" to stop the NodeBB server\n');
process.stdout.write(' "' + './nodebb log'.yellow + '" to view server output\n');
process.stdout.write(' "' + './nodebb restart'.yellow + '" to restart NodeBB\n\n'.reset);
// Spawn a new NodeBB process
cproc.fork(loaderPath, process.argv.slice(3), {
env: process.env,
});
},
},
stop: {
description: 'Stop the NodeBB server',
usage: 'Usage: ' + './nodebb stop'.yellow,
handler: function () {
getRunningPid(function (err, pid) {
if (!err) {
process.kill(pid, 'SIGTERM');
process.stdout.write('Stopping NodeBB. Goodbye!\n');
} else {
process.stdout.write('NodeBB is already stopped.\n');
}
});
},
},
restart: {
description: 'Restart the NodeBB server',
usage: 'Usage: ' + './nodebb restart'.yellow,
handler: function () {
getRunningPid(function (err, pid) {
if (!err) {
process.kill(pid, 'SIGHUP');
process.stdout.write('\nRestarting NodeBB\n'.bold);
} else {
process.stdout.write('NodeBB could not be restarted, as a running instance could not be found.\n');
}
});
},
},
log: {
description: 'Open the output log (useful for debugging)',
usage: 'Usage: ' + './nodebb log'.yellow,
handler: function () {
process.stdout.write('\nHit '.red + 'Ctrl-C '.bold + 'to exit'.red);
process.stdout.write('\n\n'.reset);
cproc.spawn('tail', ['-F', './logs/output.log'], {
cwd: __dirname,
stdio: 'inherit',
});
},
},
slog: {
description: 'Start the NodeBB server and view the live output log',
usage: 'Usage: ' + './nodebb slog'.yellow,
handler: function () {
process.stdout.write('\nStarting NodeBB with logging output\n'.bold);
process.stdout.write('\nHit '.red + 'Ctrl-C '.bold + 'to exit'.red);
process.stdout.write('\n\n'.reset);
// Spawn a new NodeBB process
cproc.fork(loaderPath, {
env: process.env,
});
cproc.spawn('tail', ['-F', './logs/output.log'], {
cwd: __dirname,
stdio: 'inherit',
});
},
},
dev: {
description: 'Start NodeBB in verbose development mode',
usage: 'Usage: ' + './nodebb dev'.yellow,
handler: function () {
process.env.NODE_ENV = 'development';
cproc.fork(loaderPath, ['--no-daemon', '--no-silent'], {
env: process.env,
});
},
},
build: {
description: 'Compile static assets (CSS, Javascript, etc)',
usage: 'Usage: ' + './nodebb build'.yellow + ' [js,clientCSS,acpCSS,tpl,lang]'.red + '\n' +
' e.g. ' + './nodebb build js,tpl'.yellow + '\tbuilds JS and templates\n' +
' ' + './nodebb build'.yellow + '\t\tbuilds all targets\n',
handler: function () {
var arr = ['--build'].concat(process.argv.slice(3));
fork(arr);
},
},
setup: {
description: 'Run the NodeBB setup script',
usage: 'Usage: ' + './nodebb setup'.yellow,
handler: function () {
var arr = ['--setup'].concat(process.argv.slice(3));
fork(arr);
},
},
reset: {
description: 'Disable plugins and restore the default theme',
usage: 'Usage: ' + './nodebb reset '.yellow + '{-t|-p|-w|-s|-a}'.red + '\n' +
' -t <theme>\tuse specified theme\n' +
' -p <plugin>\tdisable specified plugin\n' +
'\n' +
' -t\t\tuse default theme\n' +
' -p\t\tdisable all but core plugins\n' +
' -w\t\twidgets\n' +
' -s\t\tsettings\n' +
' -a\t\tall of the above\n',
handler: function () {
var arr = ['--reset'].concat(process.argv.slice(3));
fork(arr);
},
},
activate: {
description: 'Activate a plugin for the next startup of NodeBB',
usage: 'Usage: ' + './nodebb activate <plugin>'.yellow,
handler: function () {
var name = args._[1];
if (!name) {
process.stdout.write(commands.activate.usage + '\n');
process.exit();
}
if (name.startsWith('nodebb-theme')) {
fork(['--reset', '-t', name]);
return;
}
var arr = ['--activate=' + name].concat(process.argv.slice(4));
fork(arr);
},
},
plugins: {
description: 'List all installed plugins',
usage: 'Usage: ' + './nodebb plugins'.yellow,
handler: function () {
var arr = ['--plugins'].concat(process.argv.slice(3));
fork(arr);
},
},
upgrade: {
description: 'Run NodeBB upgrade scripts, ensure packages are up-to-date',
usage: 'Usage: ' + './nodebb upgrade'.yellow,
handler: function () {
if (process.argv[3]) {
process.stdout.write('\nUpdating NodeBB data store schema...\n'.yellow);
var arr = ['--upgrade'].concat(process.argv.slice(3));
var upgradeProc = fork(arr);
return upgradeProc.on('close', function (err) {
if (err) {
process.stdout.write('Error occurred during upgrade');
throw err;
}
});
}
async.series([
function (next) {
packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins();
next();
},
function (next) {
process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... \n'.yellow);
packageInstall.npmInstallProduction();
next();
},
function (next) {
process.stdout.write('OK\n'.green);
process.stdout.write('2. '.bold + 'Checking installed plugins for updates... '.yellow);
upgradePlugins(next);
},
function (next) {
process.stdout.write('3. '.bold + 'Updating NodeBB data store schema...\n'.yellow);
var arr = ['--upgrade'].concat(process.argv.slice(3));
var upgradeProc = fork(arr);
upgradeProc.on('close', next);
upgradeProc.on('error', next);
},
], function (err) {
if (err) {
process.stdout.write('Error occurred during upgrade');
throw err;
}
var message = 'NodeBB Upgrade Complete!';
// some consoles will return undefined/zero columns, so just use 2 spaces in upgrade script if we can't get our column count
var columns = process.stdout.columns;
var spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' ';
process.stdout.write('OK\n'.green);
process.stdout.write('\n' + spaces + message.green.bold + '\n\n'.reset);
});
},
},
upgradePlugins: {
hidden: true,
description: '',
handler: function () {
upgradePlugins();
},
},
events: {
description: 'Outputs the last ten (10) administrative events recorded by NodeBB',
usage: 'Usage: ' + './nodebb events'.yellow,
handler: function () {
fork(['--events']);
},
},
help: {
description: 'Display the help message for a given command',
usage: 'Usage: ' + './nodebb help <command>'.yellow,
handler: function () {
var command = commands[args._[1]];
if (command) {
process.stdout.write(command.description + '\n'.reset);
process.stdout.write(command.usage + '\n'.reset);
return;
}
var keys = Object.keys(commands).filter(function (key) {
return !commands[key].hidden;
});
process.stdout.write('\nWelcome to NodeBB\n\n'.bold);
process.stdout.write('Usage: ./nodebb {' + keys.join('|') + '}\n\n');
var usage = keys.map(function (key) {
var line = '\t' + key.yellow + (key.length < 8 ? '\t\t' : '\t');
return line + commands[key].description;
}).join('\n');
process.stdout.write(usage + '\n'.reset);
},
},
};
commands['upgrade-plugins'] = commands.upgradePlugins;
if (!commands[args._[0]]) {
commands.help.handler();
} else {
commands[args._[0]].handler();
}

@ -1 +1 @@
node ./nodebb %* @echo off && cd %~dp0 && node ./src/cli %*

@ -0,0 +1,127 @@
'use strict';
// override commander functions
// to include color styling in the output
// so the CLI looks nice
var Command = require('commander').Command;
var commandColor = 'yellow';
var optionColor = 'cyan';
var argColor = 'magenta';
var subCommandColor = 'green';
var subOptionColor = 'blue';
var subArgColor = 'red';
Command.prototype.helpInformation = function () {
var desc = [];
if (this._description) {
desc = [
' ' + this._description,
'',
];
}
var cmdName = this._name;
if (this._alias) {
cmdName = cmdName + ' | ' + this._alias;
}
var usage = [
'',
' Usage: ' + cmdName[commandColor] + ' '.reset + this.usage(),
'',
];
var cmds = [];
var commandHelp = this.commandHelp();
if (commandHelp) {
cmds = [commandHelp];
}
var options = [
'',
' Options:',
'',
'' + this.optionHelp().replace(/^/gm, ' '),
'',
];
return usage
.concat(desc)
.concat(options)
.concat(cmds)
.join('\n'.reset);
};
function humanReadableArgName(arg) {
var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']';
}
Command.prototype.usage = function () {
var args = this._args.map(function (arg) {
return humanReadableArgName(arg);
});
var usage = '[options]'[optionColor] +
(this.commands.length ? ' [command]' : '')[subCommandColor] +
(this._args.length ? ' ' + args.join(' ') : '')[argColor];
return usage;
};
function pad(str, width) {
var len = Math.max(0, width - str.length);
return str + Array(len + 1).join(' ');
}
Command.prototype.commandHelp = function () {
if (!this.commands.length) {
return '';
}
var commands = this.commands.filter(function (cmd) {
return !cmd._noHelp;
}).map(function (cmd) {
var args = cmd._args.map(function (arg) {
return humanReadableArgName(arg);
}).join(' ');
return [
cmd._name[subCommandColor] +
(cmd._alias ? ' | ' + cmd._alias : '')[subCommandColor] +
(cmd.options.length ? ' [options]' : '')[subOptionColor] +
' ' + args[subArgColor],
cmd._description,
];
});
var width = commands.reduce(function (max, command) {
return Math.max(max, command[0].length);
}, 0);
return [
'',
' Commands:',
'',
commands.map(function (cmd) {
var desc = cmd[1] ? ' ' + cmd[1] : '';
return pad(cmd[0], width) + desc;
}).join('\n').replace(/^/gm, ' '),
'',
].join('\n');
};
Command.prototype.optionHelp = function () {
var width = this.largestOptionLength();
// Append the help information
return this.options
.map(function (option) {
return pad(option.flags, width)[optionColor] + ' ' + option.description;
})
.concat([pad('-h, --help', width)[optionColor] + ' output usage information'])
.join('\n');
};

@ -0,0 +1,265 @@
'use strict';
var fs = require('fs');
var path = require('path');
var packageInstall = require('../meta/package-install');
var dirname = require('./paths').baseDir;
// check to make sure dependencies are installed
try {
fs.readFileSync(path.join(dirname, 'package.json'));
fs.readFileSync(path.join(dirname, 'node_modules/async/package.json'));
} catch (e) {
if (e.code === 'ENOENT') {
process.stdout.write('Dependencies not yet installed.\n');
process.stdout.write('Installing them now...\n\n');
packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins();
packageInstall.npmInstallProduction();
require('colors');
process.stdout.write('OK'.green + '\n'.reset);
} else {
throw e;
}
}
require('colors');
var nconf = require('nconf');
var program = require('commander');
var pkg = require('../../package.json');
var file = require('../file');
var prestart = require('../prestart');
program
.name('./nodebb')
.description('Welcome to NodeBB')
.version(pkg.version)
.option('--json-logging', 'Output to logs in JSON format', false)
.option('--log-level <level>', 'Default logging level to use', 'info')
.option('-d, --dev', 'Development mode, including verbose logging', false)
.option('-l, --log', 'Log subprocess output to console', false)
.option('-c, --config <value>', 'Specify a config file', 'config.json')
.parse(process.argv);
nconf.argv().env({
separator: '__',
});
var env = program.dev ? 'development' : (process.env.NODE_ENV || 'production');
process.env.NODE_ENV = env;
global.env = env;
prestart.setupWinston();
// Alternate configuration file support
var configFile = path.resolve(dirname, program.config);
var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database'));
prestart.loadConfig(configFile);
prestart.versionCheck();
if (!configExists && process.argv[2] !== 'setup') {
require('./setup').webInstall();
return;
}
// running commands
program
.command('start')
.description('Start the NodeBB server')
.action(function () {
require('./running').start(program);
});
program
.command('slog', null, {
noHelp: true,
})
.description('Start the NodeBB server and view the live output log')
.action(function () {
program.log = true;
require('./running').start(program);
});
program
.command('dev', null, {
noHelp: true,
})
.description('Start NodeBB in verbose development mode')
.action(function () {
program.dev = true;
process.env.NODE_ENV = 'development';
global.env = 'development';
require('./running').start(program);
});
program
.command('stop')
.description('Stop the NodeBB server')
.action(function () {
require('./running').stop(program);
});
program
.command('restart')
.description('Restart the NodeBB server')
.action(function () {
require('./running').restart(program);
});
program
.command('status')
.description('Check the running status of the NodeBB server')
.action(function () {
require('./running').status(program);
});
program
.command('log')
.description('Open the output log (useful for debugging)')
.action(function () {
require('./running').log(program);
});
// management commands
program
.command('setup')
.description('Run the NodeBB setup script')
.action(function () {
require('./setup').setup();
});
program
.command('install')
.description('Launch the NodeBB web installer for configuration setup')
.action(function () {
require('./setup').webInstall();
});
program
.command('build [targets...]')
.description('Compile static assets ' + '(JS, CSS, templates, languages, sounds)'.red)
.action(function (targets) {
require('./manage').build(targets.length ? targets : true);
})
.on('--help', function () {
require('./manage').buildTargets();
});
program
.command('activate [plugin]')
.description('Activate a plugin for the next startup of NodeBB (nodebb-plugin- prefix is optional)')
.action(function (plugin) {
require('./manage').activate(plugin);
});
program
.command('plugins')
.action(function () {
require('./manage').listPlugins();
})
.description('List all installed plugins');
program
.command('events')
.description('Outputs the last ten (10) administrative events recorded by NodeBB')
.action(function () {
require('./manage').listEvents();
});
program
.command('info')
.description('Outputs various system info')
.action(function () {
require('./manage').info();
});
// reset
var resetCommand = program.command('reset');
resetCommand
.description('Reset plugins, themes, settings, etc')
.option('-t, --theme [theme]', 'Reset to [theme] or to the default theme')
.option('-p, --plugin [plugin]', 'Disable [plugin] or all plugins')
.option('-w, --widgets', 'Disable all widgets')
.option('-s, --settings', 'Reset settings to their default values')
.option('-a, --all', 'All of the above')
.action(function (options) {
var valid = ['theme', 'plugin', 'widgets', 'settings', 'all'].some(function (x) {
return options[x];
});
if (!valid) {
process.stdout.write('\n No valid options passed in, so nothing was reset.\n'.red);
resetCommand.help();
}
require('./reset').reset(options, function (err) {
if (err) { throw err; }
require('../meta/build').buildAll(function (err) {
if (err) { throw err; }
process.exit();
});
});
});
// upgrades
program
.command('upgrade [scripts...]')
.description('Run NodeBB upgrade scripts and ensure packages are up-to-date, or run a particular upgrade script')
.option('-m, --package', 'Update package.json from defaults', false)
.option('-i, --install', 'Bringing base dependencies up to date', false)
.option('-p, --plugins', 'Check installed plugins for updates', false)
.option('-s, --schema', 'Update NodeBB data store schema', false)
.option('-b, --build', 'Rebuild assets', false)
.on('--help', function () {
process.stdout.write(
'\n' +
'When running particular upgrade scripts, options are ignored.\n' +
'By default all options are enabled. Passing any options disables that default.\n' +
'Only package and dependency updates: ' + './nodebb upgrade -mi\n'.yellow +
'Only database update: ' + './nodebb upgrade -d\n\n'.yellow
);
})
.action(function (scripts, options) {
require('./upgrade').upgrade(scripts.length ? scripts : true, options);
});
program
.command('upgrade-plugins', null, {
noHelp: true,
})
.alias('upgradePlugins')
.description('Upgrade plugins')
.action(function () {
require('./upgrade-plugins').upgradePlugins(function (err) {
if (err) {
throw err;
}
process.stdout.write('OK\n'.green);
process.exit();
});
});
program
.command('help [command]')
.description('Display help for [command]')
.action(function (name) {
if (!name) {
return program.help();
}
var command = program.commands.find(function (command) { return command._name === name; });
if (command) {
command.help();
} else {
program.help();
}
});
program
.command('*', {}, {
noHelp: true,
})
.action(function () {
program.help();
});
require('./colors');
program.executables = false;
program.parse(process.argv);

@ -0,0 +1,143 @@
'use strict';
var async = require('async');
var winston = require('winston');
var childProcess = require('child_process');
var _ = require('lodash');
var build = require('../meta/build');
var db = require('../database');
var plugins = require('../plugins');
var events = require('../events');
var reset = require('./reset');
function buildTargets() {
var aliases = build.aliases;
var length = 0;
var output = Object.keys(aliases).map(function (name) {
var arr = aliases[name];
if (name.length > length) {
length = name.length;
}
return [name, arr.join(', ')];
}).map(function (tuple) {
return ' ' + _.padEnd('"' + tuple[0] + '"', length + 2).magenta + ' | ' + tuple[1];
}).join('\n');
process.stdout.write(
'\n\n Build targets:\n' +
('\n ' + _.padEnd('Target', length + 2) + ' | Aliases').green +
'\n ------------------------------------------------------\n'.blue +
output + '\n\n'
);
}
function activate(plugin) {
if (plugin.startsWith('nodebb-theme-')) {
reset.reset({
theme: plugin,
}, function (err) {
if (err) { throw err; }
process.exit();
});
return;
}
async.waterfall([
function (next) {
db.init(next);
},
function (next) {
if (!plugin.startsWith('nodebb-')) {
// Allow omission of `nodebb-plugin-`
plugin = 'nodebb-plugin-' + plugin;
}
plugins.isInstalled(plugin, next);
},
function (isInstalled, next) {
if (!isInstalled) {
return next(new Error('plugin not installed'));
}
winston.info('Activating plugin `%s`', plugin);
db.sortedSetAdd('plugins:active', 0, plugin, next);
},
function (next) {
events.log({
type: 'plugin-activate',
text: plugin,
}, next);
},
], function (err) {
if (err) {
winston.error('An error occurred during plugin activation', err);
throw err;
}
process.exit(0);
});
}
function listPlugins() {
async.waterfall([
db.init,
function (next) {
db.getSortedSetRange('plugins:active', 0, -1, next);
},
function (plugins) {
winston.info('Active plugins: \n\t - ' + plugins.join('\n\t - '));
process.exit();
},
], function (err) {
throw err;
});
}
function listEvents() {
async.series([
db.init,
events.output,
]);
}
function info() {
async.waterfall([
function (next) {
var version = require('../../package.json').version;
process.stdout.write('\n version: ' + version);
process.stdout.write('\n Node ver: ' + process.version);
next();
},
function (next) {
process.stdout.write('\n git hash: ');
childProcess.execSync('git rev-parse HEAD', {
stdio: 'inherit',
});
next();
},
function (next) {
var config = require('../../config.json');
process.stdout.write('\n database: ' + config.database);
next();
},
db.init,
function (next) {
db.info(db.client, next);
},
function (info, next) {
process.stdout.write('\n version: ' + info.version);
process.stdout.write('\n engine: ' + info.storageEngine);
next();
},
], function (err) {
if (err) { throw err; }
process.exit();
});
}
exports.build = build.build;
exports.buildTargets = buildTargets;
exports.activate = activate;
exports.listPlugins = listPlugins;
exports.listEvents = listEvents;
exports.info = info;

@ -0,0 +1,15 @@
'use strict';
var path = require('path');
var baseDir = path.join(__dirname, '../../');
var loader = path.join(baseDir, 'loader.js');
var app = path.join(baseDir, 'app.js');
var pidfile = path.join(baseDir, 'pidfile');
module.exports = {
baseDir: baseDir,
loader: loader,
app: app,
pidfile: pidfile,
};

@ -3,59 +3,57 @@
require('colors'); require('colors');
var path = require('path'); var path = require('path');
var winston = require('winston'); var winston = require('winston');
var nconf = require('nconf');
var async = require('async'); var async = require('async');
var db = require('./database'); var fs = require('fs');
var events = require('./events');
var Reset = {}; var db = require('../database');
var events = require('../events');
var meta = require('../meta');
var plugins = require('../plugins');
var widgets = require('../widgets');
Reset.reset = function (callback) { var dirname = require('./paths').baseDir;
db.init(function (err) {
if (err) {
winston.error(err);
throw err;
}
if (nconf.get('t')) { exports.reset = function (options, callback) {
var themeId = nconf.get('t'); var map = {
theme: function (next) {
var themeId = options.theme;
if (themeId === true) { if (themeId === true) {
resetThemes(callback); resetThemes(next);
} else { } else {
if (themeId.indexOf('nodebb-') !== 0) { if (!themeId.startsWith('nodebb-theme-')) {
// Allow omission of `nodebb-theme-` // Allow omission of `nodebb-theme-`
themeId = 'nodebb-theme-' + themeId; themeId = 'nodebb-theme-' + themeId;
} }
resetTheme(themeId, callback); resetTheme(themeId, next);
} }
} else if (nconf.get('p')) { },
var pluginId = nconf.get('p'); plugin: function (next) {
var pluginId = options.plugin;
if (pluginId === true) { if (pluginId === true) {
resetPlugins(callback); resetPlugins(next);
} else { } else {
if (pluginId.indexOf('nodebb-') !== 0) { if (!pluginId.startsWith('nodebb-plugin-')) {
// Allow omission of `nodebb-plugin-` // Allow omission of `nodebb-plugin-`
pluginId = 'nodebb-plugin-' + pluginId; pluginId = 'nodebb-plugin-' + pluginId;
} }
resetPlugin(pluginId, callback); resetPlugin(pluginId, next);
}
} else if (nconf.get('w')) {
resetWidgets(callback);
} else if (nconf.get('s')) {
resetSettings(callback);
} else if (nconf.get('a')) {
require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function (err) {
if (!err) {
winston.info('[reset] Reset complete.');
} else {
winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err);
} }
},
widgets: resetWidgets,
settings: resetSettings,
all: function (next) {
async.series([resetWidgets, resetThemes, resetPlugins, resetSettings], next);
},
};
callback(); var tasks = Object.keys(map)
}); .filter(function (x) { return options[x]; })
} else { .map(function (x) { return map[x]; });
if (!tasks.length) {
process.stdout.write('\nNodeBB Reset\n'.bold); process.stdout.write('\nNodeBB Reset\n'.bold);
process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow); process.stdout.write('No arguments passed in, so nothing was reset.\n\n'.yellow);
process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red); process.stdout.write('Use ./nodebb reset ' + '{-t|-p|-w|-s|-a}\n'.red);
@ -71,11 +69,19 @@ Reset.reset = function (callback) {
process.exit(0); process.exit(0);
} }
async.series([db.init].concat(tasks), function (err) {
if (err) {
winston.error('[reset] Errors were encountered during reset', err);
throw err;
}
winston.info('[reset] Reset complete');
callback();
}); });
}; };
function resetSettings(callback) { function resetSettings(callback) {
var meta = require('./meta');
meta.configs.set('allowLocalLogin', 1, function (err) { meta.configs.set('allowLocalLogin', 1, function (err) {
winston.info('[reset] Settings reset to default'); winston.info('[reset] Settings reset to default');
callback(err); callback(err);
@ -83,10 +89,7 @@ function resetSettings(callback) {
} }
function resetTheme(themeId, callback) { function resetTheme(themeId, callback) {
var meta = require('./meta'); fs.access(path.join(dirname, 'node_modules', themeId, 'package.json'), function (err) {
var fs = require('fs');
fs.access(path.join(__dirname, '../node_modules', themeId, 'package.json'), function (err) {
if (err) { if (err) {
winston.warn('[reset] Theme `%s` is not installed on this forum', themeId); winston.warn('[reset] Theme `%s` is not installed on this forum', themeId);
callback(new Error('theme-not-found')); callback(new Error('theme-not-found'));
@ -108,8 +111,6 @@ function resetTheme(themeId, callback) {
} }
function resetThemes(callback) { function resetThemes(callback) {
var meta = require('./meta');
meta.themes.set({ meta.themes.set({
type: 'local', type: 'local',
id: 'nodebb-theme-persona', id: 'nodebb-theme-persona',
@ -163,13 +164,11 @@ function resetPlugins(callback) {
function resetWidgets(callback) { function resetWidgets(callback) {
async.waterfall([ async.waterfall([
require('./plugins').reload, plugins.reload,
require('./widgets').reset, widgets.reset,
function (next) { function (next) {
winston.info('[reset] All Widgets moved to Draft Zone'); winston.info('[reset] All Widgets moved to Draft Zone');
next(); next();
}, },
], callback); ], callback);
} }
module.exports = Reset;

@ -0,0 +1,119 @@
'use strict';
var fs = require('fs');
var childProcess = require('child_process');
var fork = require('../meta/debugFork');
var paths = require('./paths');
var dirname = paths.baseDir;
function getRunningPid(callback) {
fs.readFile(paths.pidfile, {
encoding: 'utf-8',
}, function (err, pid) {
if (err) {
return callback(err);
}
pid = parseInt(pid, 10);
try {
process.kill(pid, 0);
callback(null, pid);
} catch (e) {
callback(e);
}
});
}
function start(options) {
if (options.dev) {
process.env.NODE_ENV = 'development';
fork(paths.loader, ['--no-daemon', '--no-silent'], {
env: process.env,
cwd: dirname,
stdio: 'inherit',
});
return;
}
if (options.log) {
process.stdout.write('\nStarting NodeBB with logging output\n'.bold);
process.stdout.write('\nHit '.red + 'Ctrl-C '.bold + 'to exit'.red);
process.stdout.write('\nThe NodeBB process will continue to run in the background');
process.stdout.write('\nUse "' + './nodebb stop'.yellow + '" to stop the NodeBB server\n');
process.stdout.write('\n\n'.reset);
} else if (!options.silent) {
process.stdout.write('\nStarting NodeBB\n'.bold);
process.stdout.write(' "' + './nodebb stop'.yellow + '" to stop the NodeBB server\n');
process.stdout.write(' "' + './nodebb log'.yellow + '" to view server output\n');
process.stdout.write(' "' + './nodebb restart'.yellow + '" to restart NodeBB\n\n'.reset);
}
// Spawn a new NodeBB process
fork(paths.loader, process.argv.slice(3), {
env: process.env,
cwd: dirname,
});
if (options.log) {
childProcess.spawn('tail', ['-F', './logs/output.log'], {
cwd: dirname,
stdio: 'inherit',
});
}
}
function stop() {
getRunningPid(function (err, pid) {
if (!err) {
process.kill(pid, 'SIGTERM');
process.stdout.write('Stopping NodeBB. Goodbye!\n');
} else {
process.stdout.write('NodeBB is already stopped.\n');
}
});
}
function restart(options) {
getRunningPid(function (err, pid) {
if (!err) {
process.stdout.write('\nRestarting NodeBB\n'.bold);
process.kill(pid, 'SIGTERM');
options.silent = true;
start(options);
} else {
process.stdout.write('NodeBB could not be restarted, as a running instance could not be found.\n');
}
});
}
function status() {
getRunningPid(function (err, pid) {
if (!err) {
process.stdout.write('\nNodeBB Running '.bold + '(pid '.cyan + pid.toString().cyan + ')\n'.cyan);
process.stdout.write('\t"' + './nodebb stop'.yellow + '" to stop the NodeBB server\n');
process.stdout.write('\t"' + './nodebb log'.yellow + '" to view server output\n');
process.stdout.write('\t"' + './nodebb restart'.yellow + '" to restart NodeBB\n\n');
} else {
process.stdout.write('\nNodeBB is not running\n'.bold);
process.stdout.write('\t"' + './nodebb start'.yellow + '" to launch the NodeBB server\n\n'.reset);
}
});
}
function log() {
process.stdout.write('\nHit '.red + 'Ctrl-C '.bold + 'to exit'.red);
process.stdout.write('\n\n'.reset);
childProcess.spawn('tail', ['-F', './logs/output.log'], {
cwd: dirname,
stdio: 'inherit',
});
}
exports.start = start;
exports.stop = stop;
exports.restart = restart;
exports.status = status;
exports.log = log;

@ -0,0 +1,59 @@
'use strict';
var winston = require('winston');
var async = require('async');
var install = require('../../install/web').install;
function setup() {
var install = require('../install');
var build = require('../meta/build');
var prestart = require('../prestart');
winston.info('NodeBB Setup Triggered via Command Line');
process.stdout.write('\nWelcome to NodeBB!\n');
process.stdout.write('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.\n');
process.stdout.write('Press enter to accept the default setting (shown in brackets).\n');
async.series([
install.setup,
prestart.loadConfig,
build.buildAll,
], function (err, data) {
// Disregard build step data
data = data[0];
var separator = ' ';
if (process.stdout.columns > 10) {
for (var x = 0, cols = process.stdout.columns - 10; x < cols; x += 1) {
separator += '=';
}
}
process.stdout.write('\n' + separator + '\n\n');
if (err) {
winston.error('There was a problem completing NodeBB setup', err);
throw err;
} else {
if (data.hasOwnProperty('password')) {
process.stdout.write('An administrative user was automatically created for you:\n');
process.stdout.write(' Username: ' + data.username + '\n');
process.stdout.write(' Password: ' + data.password + '\n');
process.stdout.write('\n');
}
process.stdout.write('NodeBB Setup Completed. Run \'./nodebb start\' to manually start your NodeBB server.\n');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups)
if (process.send) {
process.send(data);
}
}
process.exit();
});
}
exports.setup = setup;
exports.webInstall = install;

@ -0,0 +1,216 @@
'use strict';
var async = require('async');
var prompt = require('prompt');
var request = require('request');
var cproc = require('child_process');
var semver = require('semver');
var fs = require('fs');
var path = require('path');
var paths = require('./paths');
var dirname = paths.baseDir;
function getModuleVersions(modules, callback) {
var versionHash = {};
async.eachLimit(modules, 50, function (module, next) {
fs.readFile(path.join(dirname, 'node_modules', module, 'package.json'), { encoding: 'utf-8' }, function (err, pkg) {
if (err) {
return next(err);
}
try {
pkg = JSON.parse(pkg);
versionHash[module] = pkg.version;
next();
} catch (err) {
next(err);
}
});
}, function (err) {
callback(err, versionHash);
});
}
function getInstalledPlugins(callback) {
async.parallel({
files: async.apply(fs.readdir, path.join(dirname, 'node_modules')),
deps: async.apply(fs.readFile, path.join(dirname, 'package.json'), { encoding: 'utf-8' }),
}, function (err, payload) {
if (err) {
return callback(err);
}
var isNbbModule = /^nodebb-(?:plugin|theme|widget|rewards)-[\w-]+$/;
var moduleName;
var isGitRepo;
payload.files = payload.files.filter(function (file) {
return isNbbModule.test(file);
});
try {
payload.deps = JSON.parse(payload.deps).dependencies;
payload.bundled = [];
payload.installed = [];
} catch (err) {
return callback(err);
}
for (moduleName in payload.deps) {
if (isNbbModule.test(moduleName)) {
payload.bundled.push(moduleName);
}
}
// Whittle down deps to send back only extraneously installed plugins/themes/etc
payload.files.forEach(function (moduleName) {
try {
fs.accessSync(path.join(dirname, 'node_modules', moduleName, '.git'));
isGitRepo = true;
} catch (e) {
isGitRepo = false;
}
if (
payload.files.indexOf(moduleName) !== -1 && // found in `node_modules/`
payload.bundled.indexOf(moduleName) === -1 && // not found in `package.json`
!fs.lstatSync(path.join(dirname, 'node_modules', moduleName)).isSymbolicLink() && // is not a symlink
!isGitRepo // .git/ does not exist, so it is not a git repository
) {
payload.installed.push(moduleName);
}
});
getModuleVersions(payload.installed, callback);
});
}
function getCurrentVersion(callback) {
fs.readFile(path.join(dirname, 'package.json'), { encoding: 'utf-8' }, function (err, pkg) {
if (err) {
return callback(err);
}
try {
pkg = JSON.parse(pkg);
} catch (err) {
return callback(err);
}
callback(null, pkg.version);
});
}
function checkPlugins(standalone, callback) {
if (standalone) {
process.stdout.write('Checking installed plugins and themes for updates... ');
}
async.waterfall([
async.apply(async.parallel, {
plugins: async.apply(getInstalledPlugins),
version: async.apply(getCurrentVersion),
}),
function (payload, next) {
var toCheck = Object.keys(payload.plugins);
if (!toCheck.length) {
process.stdout.write('OK'.green + '\n'.reset);
return next(null, []); // no extraneous plugins installed
}
request({
method: 'GET',
url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='),
json: true,
}, function (err, res, body) {
if (err) {
process.stdout.write('error'.red + '\n'.reset);
return next(err);
}
process.stdout.write('OK'.green + '\n'.reset);
if (!Array.isArray(body) && toCheck.length === 1) {
body = [body];
}
var current;
var suggested;
var upgradable = body.map(function (suggestObj) {
current = payload.plugins[suggestObj.package];
suggested = suggestObj.version;
if (suggestObj.code === 'match-found' && semver.gt(suggested, current)) {
return {
name: suggestObj.package,
current: current,
suggested: suggested,
};
}
return null;
}).filter(Boolean);
next(null, upgradable);
});
},
], callback);
}
function upgradePlugins(callback) {
var standalone = false;
if (typeof callback !== 'function') {
callback = function () {};
standalone = true;
}
checkPlugins(standalone, function (err, found) {
if (err) {
process.stdout.write('Warning'.yellow + ': An unexpected error occured when attempting to verify plugin upgradability\n'.reset);
return callback(err);
}
if (found && found.length) {
process.stdout.write('\nA total of ' + String(found.length).bold + ' package(s) can be upgraded:\n');
found.forEach(function (suggestObj) {
process.stdout.write(' * '.yellow + suggestObj.name.reset + ' (' + suggestObj.current.yellow + ' -> '.reset + suggestObj.suggested.green + ')\n'.reset);
});
process.stdout.write('\n');
} else {
if (standalone) {
process.stdout.write('\nAll packages up-to-date!'.green + '\n'.reset);
}
return callback();
}
prompt.message = '';
prompt.delimiter = '';
prompt.start();
prompt.get({
name: 'upgrade',
description: 'Proceed with upgrade (y|n)?'.reset,
type: 'string',
}, function (err, result) {
if (err) {
return callback(err);
}
if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) {
process.stdout.write('\nUpgrading packages...');
var args = ['i'];
found.forEach(function (suggestObj) {
args.push(suggestObj.name + '@' + suggestObj.suggested);
});
cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', args, { stdio: 'ignore' }, callback);
} else {
process.stdout.write('\nPackage upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade-plugins'.green + '".\n'.reset);
callback();
}
});
});
}
exports.upgradePlugins = upgradePlugins;

@ -0,0 +1,117 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var packageInstall = require('../meta/package-install');
var upgrade = require('../upgrade');
var build = require('../meta/build');
var db = require('../database');
var meta = require('../meta');
var upgradePlugins = require('./upgrade-plugins').upgradePlugins;
var steps = {
package: function (next) {
process.stdout.write('Updating package.json file with defaults... \n'.yellow);
packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins();
process.stdout.write('OK\n'.green);
next();
},
install: function (next) {
process.stdout.write('Bringing base dependencies up to date... \n'.yellow);
packageInstall.npmInstallProduction();
process.stdout.write('OK\n'.green);
next();
},
plugins: function (next) {
process.stdout.write('Checking installed plugins for updates... \n'.yellow);
async.series([
db.init,
upgradePlugins,
function (next) {
process.stdout.write('OK\n'.green);
next();
},
], next);
},
schema: function (next) {
process.stdout.write('Updating NodeBB data store schema...\n'.yellow);
async.series([
db.init,
upgrade.run,
function (next) {
process.stdout.write('OK\n'.green);
next();
},
], next);
},
build: function (next) {
process.stdout.write('Rebuilding assets...\n'.yellow);
async.series([
build.buildAll,
function (next) {
process.stdout.write('OK\n'.green);
next();
},
], next);
},
};
function runSteps(tasks) {
tasks = tasks.map(function (key, i) {
return function (next) {
process.stdout.write(((i + 1) + '. ').bold);
return steps[key](next);
};
});
async.series(tasks, function (err) {
if (err) {
process.stdout.write('Error occurred during upgrade');
throw err;
}
var message = 'NodeBB Upgrade Complete!';
// some consoles will return undefined/zero columns, so just use 2 spaces in upgrade script if we can't get our column count
var columns = process.stdout.columns;
var spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' ';
process.stdout.write('\n' + spaces + message.green.bold + '\n\n'.reset);
process.exit();
});
}
function runUpgrade(upgrades, options) {
process.stdout.write('\nUpdating NodeBB...\n'.cyan);
// disable mongo timeouts during upgrade
nconf.set('mongo:options:socketTimeoutMS', 0);
if (upgrades === true) {
var tasks = Object.keys(steps);
if (options.package || options.install ||
options.plugins || options.schema || options.build) {
tasks = tasks.filter(function (key) {
return options[key];
});
}
runSteps(tasks);
return;
}
async.series([
db.init,
meta.configs.init,
async.apply(upgrade.runParticular, upgrades),
], function (err) {
if (err) {
throw err;
}
process.exit(0);
});
}
exports.upgrade = runUpgrade;

@ -83,6 +83,8 @@ var aliases = {
sounds: ['sound'], sounds: ['sound'],
}; };
exports.aliases = aliases;
aliases = Object.keys(aliases).reduce(function (prev, key) { aliases = Object.keys(aliases).reduce(function (prev, key) {
var arr = aliases[key]; var arr = aliases[key];
arr.forEach(function (alias) { arr.forEach(function (alias) {

@ -0,0 +1,84 @@
'use strict';
var nconf = require('nconf');
var url = require('url');
var winston = require('winston');
var path = require('path');
var pkg = require('../package.json');
var dirname = require('./cli/paths').baseDir;
function setupWinston() {
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
colorize: true,
timestamp: function () {
var date = new Date();
return nconf.get('json-logging') ? date.toJSON() :
date.getDate() + '/' + (date.getMonth() + 1) + ' ' +
date.toTimeString().substr(0, 8) + ' [' + global.process.pid + ']';
},
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'),
json: !!nconf.get('json-logging'),
stringify: !!nconf.get('json-logging'),
});
}
function loadConfig(configFile) {
winston.verbose('* using configuration stored in: %s', configFile);
nconf.file({
file: configFile,
});
nconf.defaults({
base_dir: dirname,
themes_path: path.join(dirname, 'node_modules'),
upload_path: 'public/uploads',
views_dir: path.join(dirname, 'build/public/templates'),
version: pkg.version,
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
// Ensure themes_path is a full filepath
nconf.set('themes_path', path.resolve(dirname, nconf.get('themes_path')));
nconf.set('core_templates_path', path.join(dirname, 'src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
nconf.set('upload_path', path.resolve(nconf.get('base_dir'), nconf.get('upload_path')));
if (nconf.get('url')) {
nconf.set('url_parsed', url.parse(nconf.get('url')));
}
// Explicitly cast 'jobsDisabled' as Bool
var castAsBool = ['jobsDisabled'];
nconf.stores.env.readOnly = false;
castAsBool.forEach(function (prop) {
var value = nconf.get(prop);
if (value) {
nconf.set(prop, typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true');
}
});
nconf.stores.env.readOnly = true;
}
function versionCheck() {
var version = process.version.slice(1);
var range = pkg.engines.node;
var semver = require('semver');
var compatible = semver.satisfies(version, range);
if (!compatible) {
winston.warn('Your version of Node.js is too outdated for NodeBB. Please update your version of Node.js.');
winston.warn('Recommended ' + range.green + ', '.reset + version.yellow + ' provided\n'.reset);
}
}
exports.setupWinston = setupWinston;
exports.loadConfig = loadConfig;
exports.versionCheck = versionCheck;

@ -18,7 +18,7 @@ var file = require('../src/file');
* 3. Add your script under the "method" property * 3. Add your script under the "method" property
*/ */
var Upgrade = {}; var Upgrade = module.exports;
Upgrade.getAll = function (callback) { Upgrade.getAll = function (callback) {
async.waterfall([ async.waterfall([
@ -212,4 +212,3 @@ Upgrade.incrementProgress = function (value) {
process.stdout.write(' [' + (filled ? new Array(filled).join('#') : '') + new Array(unfilled).join(' ') + '] (' + this.current + '/' + (this.total || '??') + ') ' + percentage + ' '); process.stdout.write(' [' + (filled ? new Array(filled).join('#') : '') + new Array(unfilled).join(' ') + '] (' + this.current + '/' + (this.total || '??') + ') ' + percentage + ' ');
}; };
module.exports = Upgrade;

Loading…
Cancel
Save