robojones/z1

View on GitHub
cli/main.js

Summary

Maintainability
D
2 days
Test Coverage
D
68%
#! /usr/bin/env node

const path = require('path')
const program = require('commander')
const spawn = require('child_process').spawn
const colors = require('colors/safe')

// initialize global.handle and global.log methods
require('./lib/logs')

const z1 = require('..')
const getAppname = require('./lib/get-appname.js')
const features = require('./lib/features')
const parser = require('./lib/parser')
const version = require('./lib/version')
const z1Logs = require('./lib/z1-logs')
const heading = require('./lib/heading')
const logResult = require('./lib/log-result')

const SPACER = '--'

const argv = process.argv.slice()
let args = []
if (argv.includes(SPACER)) {
    args = argv.splice(argv.indexOf(SPACER))
    args.shift()
}

z1Logs(z1)

program
    .version(version.string)
    .option('-V <version>', 'version')
    .action(function (cmd) {
        handle(new Error(`command "${cmd}" not found`))
    })

program
    .command('resurrect')
    .description('start the apps that were started before exit')
    .option('-i, --immediate', 'exit immediately')
    .action((opts) => {
        z1.resurrect(opts.immediate).then(data => {
            if (opts.immediate) return
            logResult(data)
        }).catch(handle)
    })

program
    .command('start [dir]')
    .usage('[options] [dir] [-- [arguments]]')
    .description('start the app in the dir')
    .option('-n, --name <name>', 'name of your app')
    .option('-p, --ports <ports>', 'ports that your app listens to', parser.ports)
    .option('-w, --workers <workers>', 'count of workers (default: number of CPUs)', parseInt)
    .option('-o, --output <output>', 'directory for the log files of this app', parser.path)
    .option('-i, --immediate', 'exit immediately')
    .action((dir, opts) => {
        // prepare opts
        const opt = {
            name: opts.name,
            workers: opts.workers,
            ports: opts.ports,
            output: opts.output,
        }

        const env = {}

        z1.start(dir, args, opt, env, opts.immediate).then(data => {
            if (opts.immediate) return
            logResult(data)
        }).catch(handle)
    })

program
    .command('stop [appname]')
    .description('stop the app specified by the appname')
    .option('-t, --timeout <timeout>', 'time until the workers get killed')
    .option('-s, --signal <signal>', 'kill signal')
    .option('-i, --immediate', 'exit immediately')
    .action((appname = getAppname(), opts) => {
        const opt = {
            timeout: opts.timeout,
            signal: opts.signal,
        }
        z1.stop(appname, opt, opts.immediate).then(data => {
            if (opts.immediate) return
            logResult(data)
        }).catch(handle)
    })

program
    .command('restart [appname]')
    .description('restart the app specified by the appname')
    .option('-t, --timeout <timeout>', 'time until the old workers get killed')
    .option('-s, --signal <signal>', 'kill signal for the old workers')
    .option('-i, --immediate', 'exit immediately')
    .action((appname = getAppname(), opts) => {
        const opt = {
            timeout: opts.timeout,
            signal: opts.signal,
        }
        z1.restart(appname, opt, opts.immediate).then(data => {
            if (opts.immediate) return
            logResult(data)
        }).catch(handle)
    })

program
    .command('restart-all')
    .description('restart all apps')
    .option('-t, --timeout <timeout>', 'time until the old workers get killed')
    .option('-s, --signal <signal>', 'kill signal for the old workers')
    .option('-i, --immediate', 'exit immediately')
    .action(opts => {
        const opt = {
            timeout: opts.timeout,
            signal: opts.signal,
        }
        z1.restartAll(opt, opts.immediate).then(data => {
            if (opts.immediate) return
            logResult(data)
        }).catch(handle)
    })

program
    .command('logs [appname]')
    .description('show the output of an app')
    .action((appname = getAppname()) => {
        z1.logs(appname).catch(handle)
    })

program
    .command('info [appname]')
    .description('show specific infos about an app')
    .option('--name', 'output the appname')
    .option('--dir', 'output the directory of the app')
    .option('--ports', 'output the ports that the app uses')
    .option('--pending', 'output the number of pending workers')
    .option('--available', 'output the number of available workers')
    .option('--killed', 'output the number of killed workers')
    .option('--revive-count', 'output how often the app has been revived')
    .action((appname = getAppname(), opts) => {
        z1.info(appname).then(stats => {
            const props = ['name', 'dir', 'ports', 'pending', 'available', 'killed', 'reviveCount']
            const prop = props.find(prop => opts.hasOwnProperty(prop))
            if (prop) {
                const value = stats[prop]
                if (Array.isArray(value)) {
                    console.log(value.join() || '-')
                } else {
                    console.log(value)
                }
                process.exit(0)
            }

            console.log('name:', stats.name)
            console.log('directory:', stats.dir)
            console.log('ports:', stats.ports.join() || '-')
            console.log('workers:')
            console.log('  pending:', stats.pending)
            console.log('  available:', stats.available)
            console.log('  killed:', stats.killed)
            console.log('revive count:', stats.reviveCount)
        }).catch(handle)
    })

program
    .command('list')
    .description('overview of all running workers')
    .option('-m, --minimal', 'minimalistic list (easy to parse)')
    .action(opt => {
        z1.list().then(data => {
            const props = Object.keys(data.stats)

            if (opt.minimal) {
                console.log(props.join(' '))
                return
            }

            heading('workers ports     name                 directory')

            props.forEach(name => {
                const app = data.stats[name]

                let workers = colors.bold(`${app.available}/${app.workers}`.padEnd(7))
                if (app.available < app.workers) {
                    workers = colors.red(workers)
                } else if (app.available > app.workers) {
                    workers = colors.yellow(workers)
                } else {
                    workers = colors.green(workers)
                }

                const ports = app.ports.join() || '-'

                console.log(`${workers} ${ports.padEnd(9)} ${name.padEnd(20)} ${app.dir}`)
            })

            if (data.isResurrectable) {
                console.log()
                console.log(colors.dim('The listed apps are currently not running.'))
                console.log(colors.dim('You can use "z1 resurrect" to start them.'))
            }
        }).catch(handle)
    })

program
    .command('exit')
    .description('kill the z1 daemon')
    .action(() => {
        z1.exit().then(() => {
            console.log('daemon stopped')
        }).catch(handle)
    })

program
    .command('install [feature]')
    .description('install additional features')
    .option('-m, --minimal', 'minimalistic list (easy to parse)')
    .action((feature, opts) => {
        const folder = path.join(__dirname, '..', 'script', 'install')

        if (opts.minimal) {
            Object.keys(features).forEach((feature, i, list) => {
                process.stdout.write(feature)
                if (list[i + 1]) {
                    process.stdout.write(' ')
                }
            })
            process.stdout.write('\n')
        } else if (!feature) {
            console.log('\nFeatures:\n')
            Object.keys(features).forEach(feature => {
                console.log(`${feature} - ${features[feature]}`)
            })
            console.log()
        } else if (features.hasOwnProperty(feature)) {
            const file = path.join(folder, feature)
            const installer = spawn(file, [], {
                cwd: folder,
                stdio: 'inherit',
                shell: true,
            })
            installer.on('error', handle)
            installer.on('exit', (code) => {
                process.exit(code)
            })
        } else {
            handle(new Error('feature not found'))
        }
    })

program
    .command('uninstall <feature>')
    .description('uninstall features')
    .action(feature => {
        const folder = path.join(__dirname, '..', 'script', 'uninstall')

        if (features.hasOwnProperty(feature)) {
            const file = path.join(folder, feature)
            const uninstaller = spawn(file, [], {
                cwd: folder,
                stdio: 'inherit',
                shell: true,
            })
            uninstaller.on('error', handle)
            uninstaller.on('exit', (code) => {
                process.exit(code)
            })
        } else {
            handle(new Error('feature not found'))
        }
    })

if (!global.test) {
    if (process.argv.length === 2) {
        program.outputHelp()
    }

    program.parse(argv)
}