#!/usr/bin/env node 'use strict'; var fs = require('fs'); var path = require('path'); var cproc = require('child_process'); // check to make sure dependencies are installed try { 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'); cproc.execSync('npm i --production', { cwd: __dirname, stdio: [0, 1, 2], }); } 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/en/latest/installing/os.html\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, { 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('\nError'.red + ': ' + err.message + '\n'); } }); } async.series([ function (next) { process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow); cproc.exec('npm i --production', { cwd: __dirname, stdio: 'ignore' }, 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); }, ], function (err) { if (err) { process.stdout.write('\nError'.red + ': ' + err.message + '\n'); } else { 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(); }, }, 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(); }