From ae24bca16e2c4eba585cbd22c296040b78866fe8 Mon Sep 17 00:00:00 2001
From: Peter Jaszkowiak
Date: Thu, 23 Nov 2017 08:55:03 -0700
Subject: [PATCH] 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
---
app.js | 266 ++----------------
install/package.json | 2 +-
nodebb | 547 +------------------------------------
nodebb.bat | 2 +-
src/cli/colors.js | 127 +++++++++
src/cli/index.js | 265 ++++++++++++++++++
src/cli/manage.js | 143 ++++++++++
src/cli/paths.js | 15 +
src/{ => cli}/reset.js | 119 ++++----
src/cli/running.js | 119 ++++++++
src/cli/setup.js | 59 ++++
src/cli/upgrade-plugins.js | 216 +++++++++++++++
src/cli/upgrade.js | 117 ++++++++
src/meta/build.js | 2 +
src/prestart.js | 84 ++++++
src/upgrade.js | 3 +-
16 files changed, 1236 insertions(+), 850 deletions(-)
create mode 100644 src/cli/colors.js
create mode 100644 src/cli/index.js
create mode 100644 src/cli/manage.js
create mode 100644 src/cli/paths.js
rename src/{ => cli}/reset.js (53%)
create mode 100644 src/cli/running.js
create mode 100644 src/cli/setup.js
create mode 100644 src/cli/upgrade-plugins.js
create mode 100644 src/cli/upgrade.js
create mode 100644 src/prestart.js
diff --git a/app.js b/app.js
index d1bf1c7c08..7942646c21 100644
--- a/app.js
+++ b/app.js
@@ -30,47 +30,16 @@ nconf.argv().env({
separator: '__',
});
-var url = require('url');
var async = require('async');
var winston = require('winston');
var path = require('path');
-var pkg = require('./package.json');
+
var file = require('./src/file');
-var debug = require('./src/meta/debugFork').debugging;
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
-var configFile = path.join(__dirname, '/config.json');
+var configFile = path.join(__dirname, 'config.json');
if (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'));
-loadConfig();
-versionCheck();
+var prestart = require('./src/prestart');
+prestart.loadConfig(configFile);
+prestart.versionCheck();
if (!process.send) {
// If run using `node app`, log GNU copyright info along with server info
@@ -89,224 +59,40 @@ if (!process.send) {
winston.info('');
}
-
if (nconf.get('setup') || nconf.get('install')) {
- setup();
+ require('./src/cli/setup').setup();
} else if (!configExists) {
require('./install/web').install(nconf.get('port'));
} else if (nconf.get('upgrade')) {
- upgrade();
+ require('./src/cli/upgrade').upgrade(true);
} else if (nconf.get('reset')) {
- async.waterfall([
- async.apply(require('./src/reset').reset),
- async.apply(require('./src/meta/build').buildAll),
+ var options = {
+ theme: nconf.get('t'),
+ 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) {
- process.exit(err ? 1 : 0);
+ if (err) {
+ throw err;
+ }
+
+ process.exit(0);
});
} else if (nconf.get('activate')) {
- activate();
+ require('./src/cli/manage').activate(nconf.get('activate'));
} else if (nconf.get('plugins')) {
- listPlugins();
+ require('./src/cli/manage').listPlugins();
} else if (nconf.get('build')) {
require('./src/meta/build').build(nconf.get('build'));
} else if (nconf.get('events')) {
- async.series([
- async.apply(require('./src/database').init),
- async.apply(require('./src/events').output),
- ]);
+ require('./src/cli/manage').listEvents();
} else {
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);
- }
-}
diff --git a/install/package.json b/install/package.json
index fe0797d3f7..6d8b0b423f 100644
--- a/install/package.json
+++ b/install/package.json
@@ -27,6 +27,7 @@
"chart.js": "^2.7.0",
"colors": "^1.1.2",
"compression": "^1.7.1",
+ "commander": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1",
"connect-mongo": "2.0.0",
@@ -52,7 +53,6 @@
"logrotate-stream": "^0.2.5",
"lru-cache": "4.1.1",
"mime": "^2.0.3",
- "minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"mongodb": "2.2.33",
"morgan": "^1.9.0",
diff --git a/nodebb b/nodebb
index adedd30826..546e608cd2 100755
--- a/nodebb
+++ b/nodebb
@@ -2,549 +2,4 @@
'use strict';
-var fs = require('fs');
-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 \tuse specified theme\n' +
- ' -p \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 '.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 '.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();
-}
+require('./src/cli');
diff --git a/nodebb.bat b/nodebb.bat
index daaf09224f..ba0e75d249 100644
--- a/nodebb.bat
+++ b/nodebb.bat
@@ -1 +1 @@
-node ./nodebb %*
+@echo off && cd %~dp0 && node ./src/cli %*
diff --git a/src/cli/colors.js b/src/cli/colors.js
new file mode 100644
index 0000000000..bb2648e1d5
--- /dev/null
+++ b/src/cli/colors.js
@@ -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');
+};
diff --git a/src/cli/index.js b/src/cli/index.js
new file mode 100644
index 0000000000..5a1ed820e0
--- /dev/null
+++ b/src/cli/index.js
@@ -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 ', '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 ', '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);
diff --git a/src/cli/manage.js b/src/cli/manage.js
new file mode 100644
index 0000000000..393c3f0753
--- /dev/null
+++ b/src/cli/manage.js
@@ -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;
diff --git a/src/cli/paths.js b/src/cli/paths.js
new file mode 100644
index 0000000000..df5532cacd
--- /dev/null
+++ b/src/cli/paths.js
@@ -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,
+};
diff --git a/src/reset.js b/src/cli/reset.js
similarity index 53%
rename from src/reset.js
rename to src/cli/reset.js
index d45c48deb5..85831d366d 100644
--- a/src/reset.js
+++ b/src/cli/reset.js
@@ -3,79 +3,85 @@
require('colors');
var path = require('path');
var winston = require('winston');
-var nconf = require('nconf');
var async = require('async');
-var db = require('./database');
-var events = require('./events');
+var fs = require('fs');
-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) {
- db.init(function (err) {
- if (err) {
- winston.error(err);
- throw err;
- }
+var dirname = require('./paths').baseDir;
- if (nconf.get('t')) {
- var themeId = nconf.get('t');
+exports.reset = function (options, callback) {
+ var map = {
+ theme: function (next) {
+ var themeId = options.theme;
if (themeId === true) {
- resetThemes(callback);
+ resetThemes(next);
} else {
- if (themeId.indexOf('nodebb-') !== 0) {
+ if (!themeId.startsWith('nodebb-theme-')) {
// Allow omission of `nodebb-theme-`
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) {
- resetPlugins(callback);
+ resetPlugins(next);
} else {
- if (pluginId.indexOf('nodebb-') !== 0) {
+ if (!pluginId.startsWith('nodebb-plugin-')) {
// Allow omission of `nodebb-plugin-`
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);
- }
-
- callback();
- });
- } else {
- process.stdout.write('\nNodeBB Reset\n'.bold);
- 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(' -t\tthemes\n');
- process.stdout.write(' -p\tplugins\n');
- process.stdout.write(' -w\twidgets\n');
- process.stdout.write(' -s\tsettings\n');
- process.stdout.write(' -a\tall of the above\n');
-
- process.stdout.write('\nPlugin and theme reset flags (-p & -t) can take a single argument\n');
- process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions, ./nodebb reset -t nodebb-theme-persona\n');
- process.stdout.write(' Prefix is optional, e.g. ./nodebb reset -p markdown, ./nodebb reset -t persona\n');
-
- process.exit(0);
+ },
+ widgets: resetWidgets,
+ settings: resetSettings,
+ all: function (next) {
+ async.series([resetWidgets, resetThemes, resetPlugins, resetSettings], next);
+ },
+ };
+
+ var tasks = Object.keys(map)
+ .filter(function (x) { return options[x]; })
+ .map(function (x) { return map[x]; });
+
+ if (!tasks.length) {
+ process.stdout.write('\nNodeBB Reset\n'.bold);
+ 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(' -t\tthemes\n');
+ process.stdout.write(' -p\tplugins\n');
+ process.stdout.write(' -w\twidgets\n');
+ process.stdout.write(' -s\tsettings\n');
+ process.stdout.write(' -a\tall of the above\n');
+
+ process.stdout.write('\nPlugin and theme reset flags (-p & -t) can take a single argument\n');
+ process.stdout.write(' e.g. ./nodebb reset -p nodebb-plugin-mentions, ./nodebb reset -t nodebb-theme-persona\n');
+ process.stdout.write(' Prefix is optional, e.g. ./nodebb reset -p markdown, ./nodebb reset -t persona\n');
+
+ 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) {
- var meta = require('./meta');
meta.configs.set('allowLocalLogin', 1, function (err) {
winston.info('[reset] Settings reset to default');
callback(err);
@@ -83,10 +89,7 @@ function resetSettings(callback) {
}
function resetTheme(themeId, callback) {
- var meta = require('./meta');
- var fs = require('fs');
-
- fs.access(path.join(__dirname, '../node_modules', themeId, 'package.json'), function (err) {
+ fs.access(path.join(dirname, 'node_modules', themeId, 'package.json'), function (err) {
if (err) {
winston.warn('[reset] Theme `%s` is not installed on this forum', themeId);
callback(new Error('theme-not-found'));
@@ -108,8 +111,6 @@ function resetTheme(themeId, callback) {
}
function resetThemes(callback) {
- var meta = require('./meta');
-
meta.themes.set({
type: 'local',
id: 'nodebb-theme-persona',
@@ -163,13 +164,11 @@ function resetPlugins(callback) {
function resetWidgets(callback) {
async.waterfall([
- require('./plugins').reload,
- require('./widgets').reset,
+ plugins.reload,
+ widgets.reset,
function (next) {
winston.info('[reset] All Widgets moved to Draft Zone');
next();
},
], callback);
}
-
-module.exports = Reset;
diff --git a/src/cli/running.js b/src/cli/running.js
new file mode 100644
index 0000000000..edd9627ee8
--- /dev/null
+++ b/src/cli/running.js
@@ -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;
diff --git a/src/cli/setup.js b/src/cli/setup.js
new file mode 100644
index 0000000000..f10e7def9b
--- /dev/null
+++ b/src/cli/setup.js
@@ -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;
diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js
new file mode 100644
index 0000000000..db04d5cc35
--- /dev/null
+++ b/src/cli/upgrade-plugins.js
@@ -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;
diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js
new file mode 100644
index 0000000000..b2255707f4
--- /dev/null
+++ b/src/cli/upgrade.js
@@ -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;
diff --git a/src/meta/build.js b/src/meta/build.js
index b01a92cb86..df68e93375 100644
--- a/src/meta/build.js
+++ b/src/meta/build.js
@@ -83,6 +83,8 @@ var aliases = {
sounds: ['sound'],
};
+exports.aliases = aliases;
+
aliases = Object.keys(aliases).reduce(function (prev, key) {
var arr = aliases[key];
arr.forEach(function (alias) {
diff --git a/src/prestart.js b/src/prestart.js
new file mode 100644
index 0000000000..9b592cf4af
--- /dev/null
+++ b/src/prestart.js
@@ -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;
diff --git a/src/upgrade.js b/src/upgrade.js
index 7229c87227..5c9cd6ea2d 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -18,7 +18,7 @@ var file = require('../src/file');
* 3. Add your script under the "method" property
*/
-var Upgrade = {};
+var Upgrade = module.exports;
Upgrade.getAll = function (callback) {
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 + ' ');
};
-module.exports = Upgrade;