mugo

Node.js scripting library

Usage no npm install needed!

<script type="module">
  import mugo from 'https://cdn.skypack.dev/mugo';
</script>

README

Mugo

Mugo makes scripting in node.js easy and intuitive. It provides a clear API for running commands locally as well as remotely over SSH, and includes helper functions to help with common situations. It keeps the simple cases simple, while still supporting more complex uses without a heavy syntactic burden.

Mugo makes use of the new async/await features of Javascript, and so requires at least Node 8.

Examples

Local

The standard run and sudo functions return promises, so for simple cases you can just do the following:

const { run } = require('mugo');

run('whoami').then(me => {
    console.log(`Hello, ${me}!`);
})

If you want to run a series of commands and do things with the results in between, you should use the async/await features.

const { run, sudo } = require('mugo');

async function main() {
    let me = await run('whoami');
    console.log(`Hello, local ${me}!`);

    let my_info = await sudo(`grep ${me} /etc/shadow`);
    console.log('My shadow entry:');
    console.log(my_info);
}

main();

In addition to using the run and sudo functions right from the module, you can make a new local instance to use.

const mugo = require('mugo');

let local = mugo.local({/* default options */});

local.run('whoami');

You can also change the options for the default functions, or for your local instance, using the defaults method.

const mugo = require('mugo');

mugo.defaults({/* new default options */});

let local = mugo.local({/* default options */});

local.defaults({/* new default options */});

Any option you provide can be overridden for a single run or sudo.

const { run } = require('mugo');

run('cat fstab', { cwd: '/etc' });

By default, using sudo will prompt for your password as it would on the command line. You can also provide a password for sudo to use. If you provide a password and the save_pass option, it will remember that password for subsequent sudo calls.

const { sudo } = require('mugo');

// only for this call
sudo('whoami', { sudo_pass: 'supersecret' });

// for this and all subsequent calls
sudo('ls -l /root', { sudo_pass: 'supersecret', save_pass: true });

If you pass a sudo password into the default options, it will be used for every sudo call. This can be done up front when creating a new local, or after the fact using the defaults method.

const mugo = require('mugo');

let local = mugo.local({ sudo_pass: 'supersecret' })
local.sudo('whoami') // won't prompt for password

let { sudo, defaults } = mugo;
defaults({ sudo_pass: 'supersecret' });
sudo('whoami'); // won't prompt for password

There are also local sync methods, which have the same interface but return the actual results of the commands, rather than a promise.

const {run_sync, sudo_sync} = require('mugo');

let me = run_sync('whoami');
console.log(`Hello, local ${me}!`);

let my_info = sudo_sync(`grep ${me} /etc/shadow`);
console.log('My shadow entry:');
console.log(my_info);

It also supports passing in streams for stdin, stdout, and/or stderr. You can also provide a string for stdin. You can read from the stdout and stderr streams and write to the stdin stream to have asynchronous communication with a running command.

const { run } = require('mugo');
let stdin = new Readable();
let stdout = new Writable();
let stderr = new Writable();

let out = run('something-interactive', {stdin, stdout, stderr});

stdout.on('data', output => {
    if(/*something about 'output' is true*/){
        stdin.write('some input');
    }
});

await out;

Remote

The remote api mostly mirrors the local api, although there are no sync functions, and to do remote calls, you need to create a remote instance first. The simplest case just requires a uri and a password.

let r = require('mugo').remote({
    uri: 'me@example.com',
    pass: mugo.prompt_pass_sync(),
});

r.run('whoami').then(me => {
    console.log(`Hello, far-away ${me}!`);)

    r.end(); // must call end or node will hang forever
});

You can also use async/await.

let r = require('mugo').remote({
    uri: 'me@example.com',
    pass: mugo.prompt_pass_sync(),
});

async function main() {
    let me = await r.run('whoami');
    console.log(`Hello, far-away ${me}!`);

    let my_info = await r.sudo(`grep ${me} /etc/shadow`);
    console.log('My far-away shadow entry:');
    console.log(my_info);

    r.end();
}

main();

Once you have a remote instance, you can change its default settings with the defaults method, but you can't change any of the SSL settings. You can also override settings on a per-call basis, just like with the local commands.

r.defaults({/* new default options */});

r.run('some-command', {/* different options */});

The remote interface also allows you to do FTP-style put and get. These methods only take individual files. Passing in a folder for either the local or remote side will result in an error. Also note that the files should be passed in as absolute paths. If a logger is available on the remote or provided to the functions, it will be used to log the file transfer.

let r = require('mugo').remote({
    uri: 'me@example.com',
    pass: 'secret password',
});

await r.put('/local/file', '/remote/file', {/* options */});

await r.get('/remote/file', '/local/file', {/* options */});

Helper Functions

There are some helper functions available to make scripting easier.

Password Prompting

You can use the following functions to prompt for passwords

const mugo = require('mugo');

let password = mugo.prompt_pass_sync();

let r = mugo.remote({ uri: 'me@example.com', pass: password });

async function main() {
    let new_password = await mugo.prompt_pass(); // async

    await r.sudo(`change_password ${new_password}`);
}

main();

Handling Uncaught Exceptions

There is a function you can run that will set up a default exception handler so that even if your script crashes for unexpected reasons, you are still able to see the stack trace and respond to the issue.

const mugo = require('mugo');

mugo.setup_uncaught_exception_handler();

// do more stuff

Options

A number of options are available, any of which can be passed to an individual function call, to the defaults function, or to the constructor.

Local

Option Used By Description Default Value
sudo_user sudo The user to run the command as (sudo -u user) undefined
sudo_prompt sudo A string to use for the sudo prompt (sudo -p prompt) undefined
sudo_pass sudo The password to provide to sudo over stdin (sudo -S) undefined
save_pass sudo Save the provided sudo password for later sudo commands false
cwd run, sudo The working directory from which to run commands undefined
encoding run, sudo The encoding to use when getting the output from the command 'utf8'
warn_only run, sudo Don't throw an exception if the command fails false
trim run, sudo Remove the trailing newline from the output true
flatten run, sudo Remove newlines and surrounding whitespace from cmd (for prettier printing) false
return_stderr_also run, sudo Return stderr along with stdout false
stdin run, sudo The stream or string to use for stdin. undefined
stdout run, sudo The stream to use for stdout. undefined
stderr run, sudo The stream to use for stderr. undefined
shell run, sudo Shell to use when running command '/bin/bash'
shell_wrap run, sudo Wrap cmd in shell explicitly false
logger run, sudo The logger object to use new ConsoleLogger()

Remote

For remote connections, you must provide at minimum a user, host, and password or private key. You can provide that information through the uri, the individual options, or any combination thereof.

If you provide an SSH password but not a sudo password, it will use the SSH password for sudo.

Option Used By Description Default Value
uri On construction only URI to use for SSH connection (will be overridden by explicit options below) undefined
host On construction only Hostname to connect to undefined
port On construction only Port to connect on 22
user On construction only Username to connect as undefined
pass On construction only Password to connect with undefined
private_key On construction only Private Key to connect using undefined
sudo_user sudo The user to run the command as (sudo -u user) undefined
sudo_prompt sudo A string to use for the sudo prompt (sudo -p prompt) undefined
sudo_pass sudo The password to provide to sudo over stdin (sudo -S) undefined
save_pass sudo Save the provided sudo password for later sudo commands false
encoding run, sudo The encoding to use when getting the output from the command 'utf8'
warn_only run, sudo Don't throw an exception if the command fails false
trim run, sudo Remove the trailing newline from the output true
flatten run, sudo Remove newlines and surrounding whitespace from cmd (for prettier printing) false
return_stderr_also run, sudo Return stderr along with stdout false
stdin run, sudo The stream or string to use for stdin. undefined
stdout run, sudo The stream to use for stdout. undefined
stderr run, sudo The stream to use for stderr. undefined
shell run, sudo Shell to use when running command '/bin/bash'
shell_wrap run, sudo Wrap cmd in shell explicitly false
logger run, sudo, get, put The logger object to use new ConsoleLogger()
mode get, put The mode to set on the destination file. Doesn't work if the file is empty. undefined

Errors

If any of the runner functions produces an error, it will have the following fields available:

Field Description
err.exit_code The exit code of the command itself
err.stdout The standard output text produced by the command
err.stderr The standard error text produced by the command
err.message / err.toString() Some kind of human readable error message

Loggers

TODO flesh out logger documentation

class Logger {
    constructor() {

    }

    on_start(cmd, info) {

    }

    on_out(stdout, info) {

    }

    on_err(stderr, info) {

    }

    on_end(exit_code, info) {

    }
}

module.exports = {
    ConsoleLogger,
};