@serverless/cli
Advanced tools
+1
-1
| { | ||
| "name": "@serverless/cli", | ||
| "version": "2.0.22", | ||
| "version": "2.0.23", | ||
| "description": "Serverless Components CLI", | ||
@@ -5,0 +5,0 @@ "main": "./src/index.js", |
+179
-163
@@ -7,6 +7,4 @@ const os = require('os') | ||
| const prettyoutput = require('prettyoutput') | ||
| const { sleep } = require('./utils') | ||
| const packageJson = require('../package.json') | ||
| // Serverless Components CLI Colors | ||
| // CLI Colors | ||
| const grey = chalk.dim | ||
@@ -16,70 +14,54 @@ const green = chalk.rgb(0, 253, 88) | ||
| /** | ||
| * CLI | ||
| * - Controls the CLI experience in the framework. | ||
| * - Once instantiated, it starts a single, long running process. | ||
| */ | ||
| class CLI { | ||
| constructor(config = {}) { | ||
| this.command = config.command | ||
| this.version = packageJson.version | ||
| this.debugMode = config.debug || false | ||
| constructor() {} | ||
| /** | ||
| * Start | ||
| * - Starts the CLI process | ||
| */ | ||
| start(config = {}) { | ||
| // Defaults | ||
| this._ = {} | ||
| this._.entity = 'Serverless' | ||
| this._.useTimer = true | ||
| this._.startTime = Date.now() | ||
| this._.seconds = 0 | ||
| // Status defaults | ||
| this._.status = {} | ||
| this._.status.running = false | ||
| this._.status.message = 'Running' | ||
| this._.status.loadingDots = '' | ||
| this._.status.loadingDotCount = 0 | ||
| // Hide cursor always, to keep it clean | ||
| this._.status = 'Initializing' | ||
| this._.lastStatus = null | ||
| this._.debug = config.debug || false | ||
| this._.timer = config.timer || false | ||
| this._.timerStarted = Date.now() | ||
| this._.timerSeconds = 0 | ||
| this._.loadingDots = '' | ||
| this._.loadingDotCount = 0 | ||
| // Hide cursor, to keep it clean | ||
| process.stdout.write(ansiEscapes.cursorHide) | ||
| // Event Handler: Control + C | ||
| process.on('SIGINT', async () => { | ||
| if (this.isStatusEngineActive()) { | ||
| return this.statusEngineStop('cancel') | ||
| } | ||
| process.exit(1) | ||
| }) | ||
| if (this._.debug) { | ||
| // Create a white space immediately | ||
| this.log() | ||
| } | ||
| // Count seconds | ||
| // Start counting seconds | ||
| setInterval(() => { | ||
| this._.seconds = Math.floor((Date.now() - this._.startTime) / 1000) | ||
| this._.timerSeconds = Math.floor((Date.now() - this._.timerStarted) / 1000) | ||
| }, 1000) | ||
| } | ||
| getRelativeVerticalCursorPosition(contentString) { | ||
| const base = 1 | ||
| const terminalWidth = process.stdout.columns | ||
| const contentWidth = stripAnsi(contentString).length | ||
| const nudges = Math.ceil(Number(contentWidth) / Number(terminalWidth)) | ||
| return base + nudges | ||
| } | ||
| // Set Event Handler: Control + C to cancel session | ||
| process.on('SIGINT', async () => { | ||
| return this.close('cancel') | ||
| }) | ||
| async statusEngine() { | ||
| this.renderStatus() | ||
| await sleep(100) | ||
| if (this.isStatusEngineActive()) { | ||
| return this.statusEngine() | ||
| } | ||
| // Start render engine | ||
| return this._renderEngine() | ||
| } | ||
| isStatusEngineActive() { | ||
| return this._.status.running | ||
| } | ||
| statusEngineStart() { | ||
| if (this.debugMode) { | ||
| this.log() | ||
| } | ||
| this._.status.running = true | ||
| // Start Status engine | ||
| return this.statusEngine() | ||
| } | ||
| statusEngineStop(reason, message) { | ||
| this._.status.running = false | ||
| /** | ||
| * Close | ||
| * - Closes the CLI process with relevant, clean information. | ||
| */ | ||
| close(reason, message) { | ||
| if (reason === 'error') { | ||
@@ -89,6 +71,6 @@ message = red(message) | ||
| if (reason === 'cancel') { | ||
| message = red('canceled') | ||
| message = red('Canceled') | ||
| } | ||
| if (reason === 'done') { | ||
| message = green(message || 'done') | ||
| message = green(message || 'Done') | ||
| } | ||
@@ -102,9 +84,9 @@ | ||
| this.log() | ||
| let content = ' ' | ||
| if (this._.useTimer) { | ||
| content += ` ${grey(this._.seconds + 's')}` | ||
| content += ` ${grey(figures.pointerSmall)}` | ||
| let content = '' | ||
| if (this._.timer) { | ||
| content += `${grey(this._.timerSeconds + 's')}` | ||
| content += ` ${grey(figures.pointerSmall)} ` | ||
| } | ||
| content += ` ${this._.entity}` | ||
| content += ` ${grey(figures.pointerSmall)} ${message}` | ||
| content += `${this._.entity} ` | ||
| content += `${grey(figures.pointerSmall)} ${message}` | ||
| process.stdout.write(content) | ||
@@ -124,59 +106,25 @@ | ||
| renderStatus(status, entity) { | ||
| // Start Status engine, if it isn't running yet | ||
| if (!this.isStatusEngineActive()) { | ||
| this.statusEngineStart() | ||
| } | ||
| /** | ||
| * Debug Mode | ||
| * - Is debug mode enabled | ||
| */ | ||
| debugMode() { | ||
| return this._.debug | ||
| } | ||
| // Set global status | ||
| if (status) { | ||
| this._.status.message = status | ||
| } | ||
| // Set global status | ||
| if (entity) { | ||
| this._.entity = entity | ||
| } | ||
| // Loading dots | ||
| if (this._.status.loadingDotCount === 0) { | ||
| this._.status.loadingDots = `.` | ||
| } else if (this._.status.loadingDotCount === 2) { | ||
| this._.status.loadingDots = `..` | ||
| } else if (this._.status.loadingDotCount === 4) { | ||
| this._.status.loadingDots = `...` | ||
| } else if (this._.status.loadingDotCount === 6) { | ||
| this._.status.loadingDots = '' | ||
| } | ||
| this._.status.loadingDotCount++ | ||
| if (this._.status.loadingDotCount > 8) { | ||
| this._.status.loadingDotCount = 0 | ||
| } | ||
| // Clear any existing content | ||
| process.stdout.write(ansiEscapes.eraseDown) | ||
| // Write content | ||
| console.log() // eslint-disable-line | ||
| let content = ' ' | ||
| if (this._.useTimer) { | ||
| content += ` ${grey(this._.seconds + 's')}` | ||
| content += ` ${grey(figures.pointerSmall)}` | ||
| } | ||
| content += ` ${this._.entity}` | ||
| content += ` ${grey(figures.pointerSmall)} ${grey(this._.status.message)}` | ||
| content += ` ${grey(this._.status.loadingDots)}` | ||
| process.stdout.write(content) | ||
| console.log() // eslint-disable-line | ||
| // Get cursor starting position according to terminal & content width | ||
| const startingPosition = this.getRelativeVerticalCursorPosition(content) | ||
| // Put cursor to starting position for next view | ||
| process.stdout.write(ansiEscapes.cursorUp(startingPosition)) | ||
| process.stdout.write(ansiEscapes.cursorLeft) | ||
| /** | ||
| * Status | ||
| * - Update status in the CLI session | ||
| * - Renders every 100ms | ||
| */ | ||
| status(status, entity) { | ||
| this._.status = status || this._.status | ||
| this._.entity = entity || this._.entity | ||
| } | ||
| renderLog(msg) { | ||
| /** | ||
| * Log | ||
| * - Render log statements cleanly | ||
| */ | ||
| log(msg) { | ||
| if (!msg || msg == '') { | ||
@@ -189,5 +137,5 @@ console.log() // eslint-disable-line | ||
| process.stdout.write(ansiEscapes.eraseDown) | ||
| // console.log() // eslint-disable-line | ||
| console.log(` ${msg}`) // eslint-disable-line | ||
| // Write log | ||
| console.log(`${msg}`) // eslint-disable-line | ||
@@ -198,4 +146,8 @@ // Put cursor to starting position for next view | ||
| renderDebug(msg) { | ||
| if (!this.debugMode || !msg || msg == '') { | ||
| /** | ||
| * Debug | ||
| * - Render debug statements cleanly | ||
| */ | ||
| debug(msg) { | ||
| if (!this._.debug || !msg || msg == '') { | ||
| return | ||
@@ -207,3 +159,3 @@ } | ||
| console.log(` ${msg}`) // eslint-disable-line | ||
| console.log(`${msg}`) // eslint-disable-line | ||
@@ -214,3 +166,7 @@ // Put cursor to starting position for next view | ||
| renderError(error, entity) { | ||
| /** | ||
| * Error | ||
| * - Render and error and close a long-running CLI process. | ||
| */ | ||
| error(error, simple = false) { | ||
| // If no argument, skip | ||
@@ -227,20 +183,25 @@ if (!error || error === '') { | ||
| process.stdout.write(ansiEscapes.eraseDown) | ||
| console.log() // eslint-disable-line | ||
| // Write Error | ||
| if (entity) { | ||
| entity = `${red(entity)} ${red(figures.pointerSmall)} ${red(`error:`)}` | ||
| console.log(` ${entity}`) // eslint-disable-line | ||
| // Render stack trace | ||
| if (!this._.debug && simple) { | ||
| // Put cursor to starting position for next view | ||
| process.stdout.write(ansiEscapes.cursorLeft) | ||
| return this.close('error', `Error: ${error.message}`) | ||
| } else { | ||
| console.log(` ${red('error:')}`) // eslint-disable-line | ||
| } | ||
| console.log() // eslint-disable-line | ||
| console.log(``, red(error.stack)) // eslint-disable-line | ||
| delete error.name | ||
| console.log(` `, error) // eslint-disable-line | ||
| // Put cursor to starting position for next view | ||
| process.stdout.write(ansiEscapes.cursorLeft) | ||
| // Put cursor to starting position for next view | ||
| process.stdout.write(ansiEscapes.cursorLeft) | ||
| return this.close('error', `Error: ${error.message}`) | ||
| } | ||
| } | ||
| renderOutputs(outputs) { | ||
| /** | ||
| * Outputs | ||
| * - Render outputs cleanly. | ||
| */ | ||
| outputs(outputs) { | ||
| if (typeof outputs !== 'object' || Object.keys(outputs).length === 0) { | ||
@@ -252,39 +213,94 @@ return | ||
| console.log() // eslint-disable-line | ||
| process.stdout.write(prettyoutput(outputs, {}, 2)) // eslint-disable-line | ||
| process.stdout.write( | ||
| prettyoutput( | ||
| outputs, | ||
| { | ||
| colors: {} | ||
| }, | ||
| 0 | ||
| ) | ||
| ) // eslint-disable-line | ||
| } | ||
| close(reason, message) { | ||
| // Skip if not active | ||
| process.stdout.write(ansiEscapes.cursorShow) | ||
| if (!this.isStatusEngineActive()) { | ||
| console.log() // eslint-disable-line | ||
| process.exit(0) | ||
| return | ||
| /** | ||
| * Render Engine | ||
| * Repetitively renders status and more on a regular interval | ||
| */ | ||
| async _renderEngine() { | ||
| /** | ||
| * Debug Mode | ||
| */ | ||
| if (this._.debug) { | ||
| // Print Status | ||
| if (this._.status !== this._.lastStatus) { | ||
| let content = `${this._.timerSeconds}s - Status - ${this._.status}` | ||
| process.stdout.write(content + os.EOL) | ||
| this._.lastStatus = '' + this._.status | ||
| } | ||
| } | ||
| return this.statusEngineStop(reason, message) | ||
| } | ||
| // basic CLI utilities | ||
| log(msg) { | ||
| this.renderLog(msg) | ||
| } | ||
| /** | ||
| * Non-Debug Mode | ||
| */ | ||
| if (!this._.debug) { | ||
| // Update active dots | ||
| if (this._.loadingDotCount === 0) { | ||
| this._.loadingDots = `.` | ||
| } else if (this._.loadingDotCount === 2) { | ||
| this._.loadingDots = `..` | ||
| } else if (this._.loadingDotCount === 4) { | ||
| this._.loadingDots = `...` | ||
| } else if (this._.loadingDotCount === 6) { | ||
| this._.loadingDots = '' | ||
| } | ||
| this._.loadingDotCount++ | ||
| if (this._.loadingDotCount > 8) { | ||
| this._.loadingDotCount = 0 | ||
| } | ||
| debug(msg) { | ||
| this.renderDebug(msg) | ||
| } | ||
| // Clear any existing content | ||
| process.stdout.write(ansiEscapes.eraseDown) | ||
| status(status, entity) { | ||
| this.renderStatus(status, entity) | ||
| } | ||
| // Write status content | ||
| console.log() // eslint-disable-line | ||
| let content = '' | ||
| if (this._.timer) { | ||
| content += `${grey(this._.timerSeconds + 's')} ` | ||
| content += `${grey(figures.pointerSmall)} ` | ||
| } | ||
| content += `${this._.entity} ` | ||
| content += `${grey(figures.pointerSmall)} ${grey(this._.status)}` | ||
| content += ` ${grey(this._.loadingDots)}` | ||
| process.stdout.write(content) | ||
| console.log() // eslint-disable-line | ||
| error(e) { | ||
| this.renderError(e) | ||
| this.close('error', e) | ||
| // Put cursor to starting position for next view | ||
| const startingPosition = this._getRelativeVerticalCursorPosition(content) | ||
| process.stdout.write(ansiEscapes.cursorUp(startingPosition)) | ||
| process.stdout.write(ansiEscapes.cursorLeft) | ||
| } | ||
| await sleep(100) | ||
| return this._renderEngine() | ||
| } | ||
| outputs(outputs) { | ||
| return this.renderOutputs(outputs) | ||
| /** | ||
| * Get Relative Vertical Cursor Position | ||
| * Get cursor starting position according to terminal & content width | ||
| */ | ||
| _getRelativeVerticalCursorPosition(contentString) { | ||
| const base = 1 | ||
| const terminalWidth = process.stdout.columns | ||
| const contentWidth = stripAnsi(contentString).length | ||
| const nudges = Math.ceil(Number(contentWidth) / Number(terminalWidth)) | ||
| return base + nudges | ||
| } | ||
| } | ||
| module.exports = CLI | ||
| /** | ||
| * Sleep | ||
| * - Because our "utils" contains business logic (and isn't exclusive to utils), circular dependencies are created and therefore "utils" cannot be required in this module. Hence copying this here... | ||
| */ | ||
| const sleep = async (wait) => new Promise((resolve) => setTimeout(() => resolve(), wait)) | ||
| module.exports = new CLI() |
@@ -0,7 +1,11 @@ | ||
| const cli = require('../cli') | ||
| const { connect, engine, getComponentInstanceData } = require('../utils') | ||
| const { runComponent } = engine | ||
| module.exports = async (cli) => { | ||
| const res = await Promise.all([connect(cli), getComponentInstanceData(cli)]) | ||
| module.exports = async (config) => { | ||
| cli.status('Initializing') | ||
| const res = await Promise.all([ | ||
| connect(config), | ||
| getComponentInstanceData(config)]) | ||
| const socket = res[0] | ||
@@ -20,4 +24,3 @@ const componentInstanceData = res[1] | ||
| cli.outputs(outputs) | ||
| cli.close('done', 'Done') | ||
| } |
@@ -0,16 +1,17 @@ | ||
| const cli = require('../cli') | ||
| const { login } = require('@serverless/platform-sdk') | ||
| module.exports = async (cli) => { | ||
| module.exports = async (config) => { | ||
| process.env.DISPLAY = true | ||
| cli.status('browser login', 'serverless') | ||
| // Disable timer | ||
| config.timer = false | ||
| cli.status('Logging in via browser') | ||
| const res = await login() | ||
| const { username } = res.users[res.userId] | ||
| // console.log(JSON.stringify(res, null, 4)) | ||
| cli.status('logged in', username) | ||
| cli.close('done', `logged in`) | ||
| cli.status('Logged in') | ||
| cli.close('done', `Successfully logged into org "${username}"`) | ||
| } |
| const args = require('minimist')(process.argv.slice(2)) | ||
| const path = require('path') | ||
| const { tmpdir } = require('os') | ||
| const cli = require('../cli') | ||
| const { | ||
@@ -12,3 +13,3 @@ getConfig, | ||
| module.exports = async (cli) => { | ||
| module.exports = async () => { | ||
| const serverlessComponentFile = getConfig('serverless.component') | ||
@@ -27,2 +28,3 @@ | ||
| } | ||
| if (!serverlessComponentFile.version || args.dev) { | ||
@@ -35,3 +37,3 @@ serverlessComponentFile.version = 'dev' | ||
| const componentDirectoryPath = process.cwd() | ||
| // Get Component path and temporary path for packaging | ||
| const componentPackagePath = path.join( | ||
@@ -43,2 +45,8 @@ tmpdir(), | ||
| ) | ||
| let componentDirectoryPath | ||
| if (serverlessComponentFile.main) { | ||
| componentDirectoryPath = path.resolve(process.cwd(), serverlessComponentFile.main) | ||
| } else { | ||
| componentDirectoryPath = process.cwd() | ||
| } | ||
@@ -45,0 +53,0 @@ cli.debug(`Packaging component from ${componentDirectoryPath}`) |
+13
-9
@@ -5,20 +5,24 @@ const args = require('minimist')(process.argv.slice(2)) | ||
| const commands = require('./commands') | ||
| const CLI = require('./CLI') | ||
| const cli = require('./cli') | ||
| // keeping it backward compatible | ||
| // Keeping it backward compatible | ||
| const runningComponents = () => isComponentsProject() | ||
| const runComponents = async () => { | ||
| const command = args._[0] | ||
| const debug = args.debug ? true : false | ||
| const cli = new CLI({ debug, command }) | ||
| const config = {} | ||
| config.command = args._[0] || 'deploy' | ||
| config.debug = args.debug ? true : false | ||
| config.timer = commands[config.command] ? false : true | ||
| // Start CLI process | ||
| cli.start(config) | ||
| try { | ||
| if (commands[command]) { | ||
| await commands[command](cli) | ||
| if (commands[config.command]) { | ||
| await commands[config.command](config) | ||
| } else { | ||
| await commands.custom(cli) | ||
| await commands.custom(config) | ||
| } | ||
| } catch (e) { | ||
| cli.error(e) | ||
| return cli.error(e) | ||
| } | ||
@@ -25,0 +29,0 @@ } |
+35
-24
@@ -21,5 +21,5 @@ const args = require('minimist')(process.argv.slice(2)) | ||
| const { merge, endsWith, contains, isNil, last, split } = require('ramda') | ||
| const cli = require('./cli') | ||
| const getEndpoints = () => { | ||
| // todo change the default stage to be prod | ||
| let stage = 'prod' | ||
@@ -97,3 +97,3 @@ if (process.env.SERVERLESS_PLATFORM_STAGE && process.env.SERVERLESS_PLATFORM_STAGE !== 'prod') { | ||
| const connect = async (cli) => { | ||
| const connect = async () => { | ||
| if (!cli.debugMode) { | ||
@@ -103,3 +103,3 @@ return | ||
| cli.status('Connecting') | ||
| cli.debug('Establishing streaming connection') | ||
@@ -354,3 +354,3 @@ const endpoints = getEndpoints() | ||
| const uploadComponentSrc = async (src, accessKey, org, cli) => { | ||
| const uploadComponentSrc = async (src, accessKey, org) => { | ||
| const { getPackageUrls } = engine | ||
@@ -365,3 +365,3 @@ | ||
| cli.debug(`packaging from ${src} into ${packagePath}`) | ||
| cli.debug(`Packaging from ${src} into ${packagePath}`) | ||
| cli.status('Packaging') | ||
@@ -374,5 +374,5 @@ | ||
| cli.status('Uploading') | ||
| cli.debug(`uploading ${packagePath} to ${packageUrls.upload.split('?')[0]}`) | ||
| cli.debug(`Uploading ${packagePath} to ${packageUrls.upload.split('?')[0]}`) | ||
| await putPackage(packagePath, packageUrls.upload) | ||
| cli.debug(`upload completed`) | ||
| cli.debug(`Upload completed`) | ||
@@ -382,3 +382,3 @@ return packageUrls.download | ||
| const resolveComponentSrcInput = async (inputs, accessKey, org, cli) => { | ||
| const resolveComponentSrcInput = async (inputs, accessKey, org) => { | ||
| let uploadDirectoryPath | ||
@@ -407,3 +407,3 @@ | ||
| inputs.src = await uploadComponentSrc(uploadDirectoryPath, accessKey, org, cli) | ||
| inputs.src = await uploadComponentSrc(uploadDirectoryPath, accessKey, org) | ||
@@ -414,4 +414,11 @@ return inputs | ||
| const getOrCreateAccessKey = async (org) => { | ||
| cli.status('Preparing') | ||
| const userConfigFile = readConfigFile() | ||
| // Verify config file | ||
| if (!userConfigFile || !userConfigFile.users || !userConfigFile.users[userConfigFile.userId]) { | ||
| cli.error(`Run 'serverless login' first to rapidly deploy your serverless application.`, true) | ||
| } | ||
| const user = userConfigFile.users[userConfigFile.userId] | ||
@@ -430,3 +437,3 @@ | ||
| const getComponentInstanceData = async (cli) => { | ||
| const getComponentInstanceData = async (config) => { | ||
| const serverlessFile = getConfig('serverless') | ||
@@ -440,3 +447,3 @@ | ||
| const { app, stage, name, component, inputs } = resolvedServerlessFile | ||
| const { org, app, stage, name, component, inputs } = resolvedServerlessFile | ||
@@ -455,18 +462,22 @@ if (typeof app === 'undefined') { | ||
| if (typeof app !== 'string' || app.split('/').length !== 2) { | ||
| throw new Error(`"${app}" is not a valid org/app`) | ||
| } | ||
| const data = { | ||
| org: app.split('/')[0], | ||
| app: app.split('/')[1], | ||
| stage: stage, | ||
| org, | ||
| app, | ||
| stage: stage || 'dev', // Default to "dev" stage | ||
| name, | ||
| method: cli.command, | ||
| debugMode: cli.debugMode, | ||
| method: config.command, | ||
| debugMode: config.debug, | ||
| credentials: getCredentials(), | ||
| accessKey: await getOrCreateAccessKey(app.split('/')[0]), | ||
| inputs | ||
| inputs: config.command === 'deploy' ? inputs : {} // Inputs are only for the "deploy" command | ||
| } | ||
| // Support for specifying "org" and "app" like: app: "myOrg/myApp" | ||
| if (data.app.includes('/')) { | ||
| data.org = data.app.split('/')[0] | ||
| data.app = data.app.split('/')[1] | ||
| } | ||
| // Get Serverless Framework access key | ||
| data.accessKey = await getOrCreateAccessKey(data.org) | ||
| if (component.split('@').length === 2) { | ||
@@ -484,4 +495,4 @@ data.componentName = component.split('@')[0] | ||
| if (inputs && inputs.src) { | ||
| data.inputs = await resolveComponentSrcInput(inputs, data.accessKey, data.org, cli) | ||
| if (data.inputs && data.inputs.src) { | ||
| data.inputs = await resolveComponentSrcInput(inputs, data.accessKey, data.org) | ||
| } | ||
@@ -488,0 +499,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
45027
2.94%920
5.38%