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;