diff --git a/lib/Serverless.js b/lib/Serverless.js index 7b2ad8b65..99f166250 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -3,6 +3,7 @@ const path = require('path'); const BbPromise = require('bluebird'); const os = require('os'); +const chalk = require('chalk'); const updateNotifier = require('update-notifier'); const minimist = require('minimist'); const pkg = require('../package.json'); @@ -17,6 +18,8 @@ const ServerlessError = require('./classes/Error').ServerlessError; const Version = require('./../package.json').version; const isStandaloneExecutable = require('./utils/isStandaloneExecutable'); +const installationMaintananceCommands = new Set(['upgrade']); + class Serverless { constructor(config) { let configObject = config; @@ -70,8 +73,20 @@ class Serverless { this.pluginManager.setCliOptions(this.processedInput.options); this.pluginManager.setCliCommands(this.processedInput.commands); - // Check if update is available - updateNotifier({ pkg }).notify(); + if (!installationMaintananceCommands.has(this.processedInput.commands[0])) { + // Check if update is available + const notifier = updateNotifier({ pkg }); + notifier.notify({ + message: + isStandaloneExecutable && notifier.update + ? `Update available ${chalk().dim(notifier.update.current)}${chalk().reset( + ' → ' + )}${chalk().green(notifier.update.latest)} \nRun ${chalk().cyan( + 'serverless upgrade' + )} to update` + : null, + }); + } return this.service.load(this.processedInput.options); }) diff --git a/lib/plugins/executable/index.js b/lib/plugins/executable/index.js new file mode 100644 index 000000000..cc080b622 --- /dev/null +++ b/lib/plugins/executable/index.js @@ -0,0 +1,98 @@ +'use strict'; + +const BbPromise = require('bluebird'); +const fs = require('fs'); +const os = require('os'); +const streamPromise = require('stream-promise'); +const fse = BbPromise.promisifyAll(require('fs-extra')); +const isStandaloneExecutable = + require('../../utils/isStandaloneExecutable') && process.platform !== 'win32'; +const currentVersion = require('../../../package').version; +const fetch = require('node-fetch'); + +const BINARIES_DIR_PATH = `${os.homedir()}/.serverless/bin`; +const BINARY_TMP_PATH = `${BINARIES_DIR_PATH}/serverless-tmp`; +const BINARY_PATH = `${BINARIES_DIR_PATH}/serverless`; + +module.exports = class Executable { + constructor(serverless) { + this.serverless = serverless; + + this.commands = { + upgrade: { + isHidden: !isStandaloneExecutable, + usage: 'Upgrade Serverless', + lifecycleEvents: ['upgrade'], + }, + }; + + this.hooks = { + 'upgrade:upgrade': () => { + return isStandaloneExecutable ? this.upgrade() : this.rejectCommand('upgrade'); + }, + }; + } + + upgrade() { + return fetch('https://api.github.com/repos/serverless/serverless/releases/latest') + .then(response => { + if (!response.ok) { + throw new this.serverless.classes.Error( + 'Sorry unable to `upgrade` at this point ' + + `(server rejected request with ${response.status})` + ); + } + return response.json(); + }) + .then(({ tag_name: tagName }) => { + const latestVersion = tagName.slice(1); + if (latestVersion === currentVersion) { + this.serverless.cli.log('Already at latest version'); + return null; + } + const platform = (() => { + switch (process.platform) { + case 'darwin': + return 'macos'; + default: + return process.platform; + } + })(); + const arch = (() => { + switch (process.arch) { + case 'x32': + return 'x86'; + case 'arm': + case 'arm64': + return 'armv6'; + default: + return process.arch; + } + })(); + this.serverless.cli.log('Downloading new version...'); + return fetch( + `https://github.com/serverless/serverless/releases/download/${tagName}/` + + `serverless-${platform}-${arch}` + ) + .then(response => { + if (!response.ok) { + throw new this.serverless.classes.Error( + 'Sorry unable to `upgrade` at this point ' + + `(server rejected request with ${response.status})` + ); + } + return streamPromise(response.body.pipe(fs.createWriteStream(BINARY_TMP_PATH))) + .then(() => fse.renameAsync(BINARY_TMP_PATH, BINARY_PATH)) + .then(() => fse.chmodAsync(BINARY_PATH, 0o755)); + }) + .then(() => this.serverless.cli.log(`Successfully upgraded to ${tagName}`)); + }); + } + + rejectCommand(command) { + throw new this.serverless.classes.Error( + `\`${command}\` command is supported only in context of a standalone exacutable instance ` + + 'in non Windows enviroment.' + ); + } +}; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index d75a45738..4be113a03 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -53,4 +53,5 @@ module.exports = [ require('./aws/deployFunction/index.js'), require('./aws/deployList/index.js'), require('./aws/invokeLocal/index.js'), + require('./executable'), ]; diff --git a/package.json b/package.json index f94578e63..66947703c 100644 --- a/package.json +++ b/package.json @@ -197,6 +197,7 @@ "replaceall": "^0.1.6", "semver": "^5.7.1", "semver-regex": "^1.0.0", + "stream-promise": "^3.2.0", "tabtab": "^3.0.2", "untildify": "^3.0.3", "update-notifier": "^2.5.0",