extensions/cli/cli.js

'use strict'

/**
 * The CLI helper used by the CLI extension.
 *
 * @module extensions/Cli/Cli
 */

const { spawn } = require('child_process')
const path = require('path')

/**
 * Cli extension.
 *
 * @class
 */
class Cli {
    constructor() {
        /**
         * The Current Working Directory.
         *
         * @type {string}
         */
        this.cwd = process.cwd()

        /**
         * An object containing environment variables to inject when running your command.
         *
         * @type {Object}
         */
        this.env = {}

        /** @type {string} */
        this.killSignal = null

        /** @type {number} */
        this.killDelay = 0

        /**
         * Latest command execution exit code.
         *
         * @type {number}
         */
        this.exitCode = null

        /**
         * The command's output.
         *
         * @type {string}
         */
        this.stdout = ''

        /**
         * The command's error output.
         *
         * @type {string}
         */
        this.stderr = ''
    }

    /**
     * Sets the Current Working Directory for the command.
     *
     * @param {string} cwd - The new CWD
     */
    setCwd(cwd) {
        if (cwd.indexOf('/') === 0) {
            this.cwd = cwd
        } else {
            this.cwd = path.join(process.cwd(), cwd)
        }
    }

    /**
     * Returns Current Working Directory.
     *
     * @return {string}
     */
    getCwd() {
        return this.cwd
    }

    /**
     * Defines environment variables.
     * Beware that all existing ones will be overridden!
     *
     * @param {Object} env - The environment variables object
     */
    setEnvironmentVariables(env) {
        this.env = env
    }

    /**
     * Defines a single environment variable.
     *
     * @param {string} name  - The environment variable name
     * @param {string} value - The value associated to the variable
     */
    setEnvironmentVariable(name, value) {
        this.env[name] = value
    }

    scheduleKillProcess(delay, signal) {
        this.killDelay = delay
        this.killSignal = signal
    }

    /**
     * Returns latest command execution exit code.
     *
     * @return {number} The exit code
     */
    getExitCode() {
        return this.exitCode
    }

    /**
     * Returns captured output.
     *
     * @throws {TypeError} Argument `type` must be one of: 'stdout', 'stderr'
     * @param {string} [type=stdout] - The standard stream type
     * @returns {string} The captured output
     */
    getOutput(type = 'stdout') {
        if (type === 'stdout') return this.stdout
        else if (type === 'stderr') return this.stderr

        throw new TypeError(`invalid output type '${type}', must be one of: 'stdout', 'stderr'`)
    }

    /**
     * Resets the Cli helper:
     * - CWD is reset to current process CWD
     * - environment variables
     * - killDelay & killSignal are disabled
     * - exitCode is set to null
     * - stdout is set to an empty string
     * - stderr is set to an empty string
     */
    reset() {
        this.cwd = process.cwd()
        this.env = {}
        this.killDelay = 0
        this.killSignal = null
        this.exitCode = null
        this.stdout = ''
        this.stderr = ''
    }

    /**
     * Run given command.
     *
     * @param {string} rawCommand - The command string
     * @returns {Promise.<boolean>} The resulting `Promise`
     */
    run(rawCommand) {
        const [command, ...args] = rawCommand.split(' ')

        return new Promise((resolve, reject) => {
            // we inherit from current env vars
            // otherwise, we can have problem with PATH
            const cmd = spawn(command, args, {
                cwd: this.cwd,
                env: Object.assign({}, process.env, this.env)
            })

            let killer
            let killed = false
            if (this.killSignal !== null) {
                killer = setTimeout(() => {
                    cmd.kill(this.killSignal)
                    killed = true
                }, this.killDelay)
            }

            const cmdStdout = []
            const cmdStderr = []

            cmd.stdout.on('data', cmdStdout.push.bind(cmdStdout))
            cmd.stderr.on('data', cmdStderr.push.bind(cmdStderr))

            cmd.on('close', (code, signal) => {
                if (killer !== undefined) {
                    if (killed !== true) {
                        clearTimeout(killer)

                        return reject(
                            new Error(
                                `process.kill('${this
                                    .killSignal}') scheduled but process exited (delay: ${this
                                    .killDelay}ms)`
                            )
                        )
                    }
                }

                this.exitCode = code

                this.stdout = Buffer.concat(cmdStdout).toString()
                this.stderr = Buffer.concat(cmdStderr).toString()

                resolve(true)
            })
        })
    }
}

/**
 * Create a new isolated Cli
 * @return {Cli}
 */
module.exports = function(...args) {
    return new Cli(...args)
}

/**
 * Cli extension.
 * @type {Cli}
 */
module.exports.Cli = Cli