isnodeonline updates

 - rename command to isonline
 - accept multiple nodes
 - support json output
 - support for silent mode which is useful if used programatically
diff --git a/doc/api/couchadmin-is-node-online.md b/doc/api/couchadmin-is-node-online.md
deleted file mode 100644
index 8d6ce94..0000000
--- a/doc/api/couchadmin-is-node-online.md
+++ /dev/null
@@ -1,18 +0,0 @@
-couchadmin-is-node-online(3) - check if a cluster node is online
-================================================================
-
-### SYNOPSIS
-
-    couchadmin.commands.isnodeonline(url)
-
-
-### DESCRIPTION
-
-Check if a given node is online / available on the current network.
-
-The command takes one argument: `url` - the url of the node to check as
-a `String`.
-
-The command returns a promise which will return a `Boolean` once the
-promise is resolved after making the request to the node. `true`
-indicates an online, available node.
diff --git a/doc/api/couchadmin-is-online.md b/doc/api/couchadmin-is-online.md
new file mode 100644
index 0000000..a97bc68
--- /dev/null
+++ b/doc/api/couchadmin-is-online.md
@@ -0,0 +1,30 @@
+couchadmin-is-online(3) - check if a cluster node is online
+===========================================================
+
+### SYNOPSIS
+
+    couchadmin.commands.isonline(urls, opts)
+
+
+### DESCRIPTION
+
+Check if nodes are online / available on the current network.
+
+The command takes two arguments: `urls` - the urls of the nodes to
+check as an `Array`. The second argument must be an `Object` providing
+options
+
+The command returns a promise which will return an `Object` where the
+keys are the provided urls and the values have the type `Boolean`.
+`true` indicates an online, available node.
+
+### Options
+
+#### json
+
+If `json` is `true` the output is logged to stdout console as json
+output.
+
+#### silent
+
+If the option `silent` is `true`, no output will be logged to stdout.
diff --git a/doc/cli/couchadmin-is-online.md b/doc/cli/couchadmin-is-online.md
new file mode 100644
index 0000000..cf363d4
--- /dev/null
+++ b/doc/cli/couchadmin-is-online.md
@@ -0,0 +1,15 @@
+couchadmin-is-online(1) - check if a cluster node is online
+===========================================================
+
+### SYNOPSIS
+
+    couchadmin isonline <url> [<url>, <url> ...] [--json]
+    couchadmin on <url> [<url>, <url> ...] [--json]
+
+
+### DESCRIPTION
+
+Check if one or several nodes are currently online or available.
+
+It will print the result as colored output. JSON output is also
+supported by passing the `--json` flag.
diff --git a/src/api.js b/src/api.js
deleted file mode 100644
index c60db9b..0000000
--- a/src/api.js
+++ /dev/null
@@ -1,37 +0,0 @@
-'use strict';
-
-import * as utils from './utils.js';
-import log from 'npmlog';
-import request from 'request';
-import Promise from 'bluebird';
-
-export const isnodeonline = function isNodeOnline (url) {
-  return new Promise(function (resolve, reject) {
-    const er = utils.checkUrl(url);
-    if (er) {
-      return reject(er);
-    }
-
-    log.http('request', 'GET', url);
-
-    request(url, (err, res, body) => {
-      if (err && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND')) {
-        log.info('nodestatus', 'NOT OK', 'node is probably offline');
-        return resolve(false);
-      }
-
-      if (err) {
-        return reject(err);
-      }
-
-      const online = res.statusCode < 300;
-
-      if (online) {
-        log.info('nodestatus', 'OK', 'node seems to be online');
-        return resolve(online);
-      }
-      log.info('nodestatus', 'NOT OK', 'node is probably offline');
-      resolve(online);
-    });
-  });
-}
diff --git a/src/isonline.js b/src/isonline.js
new file mode 100644
index 0000000..71b110a
--- /dev/null
+++ b/src/isonline.js
@@ -0,0 +1,76 @@
+import * as utils from './utils.js';
+import log from 'npmlog';
+import request from 'request';
+import Promise from 'bluebird';
+import assert from 'assert';
+
+export default isonline;
+
+function isonline (urls, opts = {silent: false, json: false}) {
+  return new Promise((resolve, reject) => {
+    if (!Array.isArray(urls)) {
+      urls = [urls];
+    }
+
+    const urlPromises = urls.map((url) => {
+      return isNodeOnline(url);
+    });
+
+    Promise.all(urlPromises).then((results) => {
+      results = results.reduce((acc, el) => {
+        const key = Object.keys(el)[0];
+        acc[key] = el[key];
+        return acc;
+      }, {});
+
+      if (opts.silent && opts.json) {
+        let msg = 'silent and json are not supported at the same time';
+        const err = new Error(msg);
+        return reject(err);
+      }
+
+      if (opts.silent) {
+        return resolve(results);
+      }
+
+      if (opts.json) {
+        console.log(results);
+        return resolve(results);
+      }
+
+      Object.keys(results).forEach((entry) => {
+        let msg;
+        if (results[entry]) {
+          msg = 'seems to be online';
+        }
+        msg = 'seems to be offline';
+        log.info('status', entry, msg);
+      });
+      return resolve(results);
+    }, reject);
+  });
+};
+
+function isNodeOnline (url) {
+  return new Promise((resolve, reject) => {
+    const er = utils.checkUrl(url);
+    if (er) {
+      return reject(er);
+    }
+
+    log.http('request', 'GET', url);
+
+    request(url, (err, res, body) => {
+      if (err && (err.code === 'ECONNREFUSED'
+        || err.code === 'ENOTFOUND')) {
+        return resolve({[url]: false});
+      }
+
+      if (err) {
+        return reject(err);
+      }
+
+      resolve({[url]: res.statusCode < 300});
+    });
+  });
+}
diff --git a/src/utils.js b/src/utils.js
index f88ce08..a806d21 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -22,7 +22,7 @@
 }
 
 export function sendJsonToNode (url, json) {
-  return new Promise(function (resolve, reject) {
+  return new Promise((resolve, reject) => {
     assert.equal(typeof json, 'object', 'argument must be an object');
 
     log.http('request', 'POST', url);
diff --git a/test/api.js b/test/api.js
deleted file mode 100644
index 9132467..0000000
--- a/test/api.js
+++ /dev/null
@@ -1,80 +0,0 @@
-'use strict';
-
-import assert from 'assert';
-import Lab from 'lab';
-
-import * as api from '../src/api.js';
-import * as common from './common.js';
-
-
-export let lab = Lab.script();
-
-lab.experiment('isnodeonline', () => {
-
-  let server;
-
-  lab.beforeEach((done) => {
-    server = common.createTestServer(done);
-  });
-
-  lab.afterEach((done) => {
-    server.close(done);
-  });
-
-
-  lab.test('rejects the promise for connection errors', (done) => {
-    api
-      .isnodeonline(common.NODE + '/socketclose')
-      .catch((err) => {
-        assert.ok(err instanceof Error);
-        done();
-      });
-  });
-
-
-  lab.test('returns error on no value provided', (done) => {
-    api
-      .isnodeonline()
-      .catch((err) => {
-        assert.ok(err instanceof Error);
-        done();
-      });
-  });
-
-  lab.test('returns error for all other errors', (done) => {
-    api
-      .isnodeonline({})
-      .catch((err) => {
-        assert.ok(err instanceof Error);
-        done();
-      });
-  });
-
-  lab.test('returns false for down site', (done) => {
-    api
-      .isnodeonline('http://127.0.0.1:65516')
-      .then((res) => {
-        assert.equal(res, false);
-        done();
-      });
-  });
-
-  lab.test('returns false for down site with bad DNS', (done) => {
-    api
-      .isnodeonline('http://example.neverexists')
-      .then((res) => {
-        assert.equal(res, false);
-        done();
-      });
-  });
-
-  lab.test('returns true for online site', (done) => {
-    api
-      .isnodeonline(common.NODE)
-      .then((res) => {
-        assert.equal(res, true);
-        done();
-      });
-  });
-
-});
diff --git a/test/common.js b/test/common.js
index c10926c..95974cd 100644
--- a/test/common.js
+++ b/test/common.js
@@ -1,17 +1,39 @@
 import http from 'http';
+import Promise from 'bluebird';
 
 export const PORT = 1337;
+export const PORT_TWO = 1338;
+
 
 export const NODE = 'http://127.0.0.1:' + PORT;
+export const NODE_TWO = 'http://127.0.0.1:' + PORT_TWO;
 
-export function createTestServer (done) {
-  return http.createServer((req, res) => {
 
-    if (req.url === '/socketclose') {
-      res.write('a');
-    }
+export function createTestServers () {
+  const promises = [PORT, PORT_TWO].map((port) => {
+    return createTestServer(port);
+  });
 
-    res.writeHead(200, {'Content-Type': 'text/plain'});
-    res.end('Hello World\n');
-  }).listen(PORT, '127.0.0.1', done);
+  return Promise.all(promises);
+}
+
+export function stopTestServers (servers) {
+  const servers = servers.map((s) => {
+    const close = Promise.promisify(s.close).bind(s);
+    return close();
+  });
+  return Promise.all(servers);
+}
+
+export function createTestServer (port) {
+  return new Promise((resolve) => {
+    const s = http.createServer((req, res) => {
+      if (req.url === '/socketclose') {
+        res.write('a');
+      }
+
+      res.writeHead(200, {'Content-Type': 'text/plain'});
+      res.end('Hello World\n');
+    }).listen(port, '127.0.0.1', () => { resolve(s); });
+  });
 }
diff --git a/test/isonline.js b/test/isonline.js
new file mode 100644
index 0000000..b12db94
--- /dev/null
+++ b/test/isonline.js
@@ -0,0 +1,170 @@
+import assert from 'assert';
+import Lab from 'lab';
+
+import isonline from '../src/isonline.js';
+import * as common from './common.js';
+import log from 'npmlog';
+
+
+export let lab = Lab.script();
+
+lab.experiment('isonline', () => {
+
+  let servers;
+
+  lab.beforeEach((done) => {
+    common.createTestServers().done((s) => {
+      servers = s;
+      done();
+    });
+  });
+
+  lab.afterEach((done) => {
+    common.stopTestServers(servers).then((res) => {
+      done();
+    });
+  });
+
+
+  lab.test('rejects the promise for connection errors', (done) => {
+    isonline(common.NODE + '/socketclose')
+      .catch((err) => {
+        assert.ok(err instanceof Error);
+        done();
+      });
+  });
+
+  lab.test('returns error on no value provided', (done) => {
+    isonline()
+      .catch((err) => {
+        assert.ok(err instanceof Error);
+        done();
+      });
+  });
+
+  lab.test('returns error for all other errors', (done) => {
+    isonline({})
+      .catch((err) => {
+        assert.ok(err instanceof Error);
+        done();
+      });
+  });
+
+  lab.test('returns false for down site', (done) => {
+    isonline('http://127.0.0.1:65516')
+      .then((res) => {
+        assert.deepEqual(res, {'http://127.0.0.1:65516': false});
+        done();
+      });
+  });
+
+  lab.test('returns false for down site with bad DNS', (done) => {
+    isonline('http://exampleneverexists')
+      .then((res) => {
+        assert.deepEqual(res, {'http://exampleneverexists': false});
+        done();
+      });
+  });
+
+  lab.test('returns true for online site', (done) => {
+    isonline(common.NODE)
+      .then((res) => {
+        assert.deepEqual(res, {[common.NODE]: true});
+        done();
+      });
+  });
+
+  lab.test('accepts multiple sites', (done) => {
+    isonline([common.NODE, common.NODE_TWO])
+      .then((res) => {
+        assert.deepEqual(res, {[common.NODE]: true, [common.NODE_TWO]: true});
+        done();
+      });
+  });
+
+  lab.test('it logs per default', (done) => {
+    let msgs = '';
+    log.once('log', ({message: message}) => {
+      msgs += message;
+    });
+    isonline(common.NODE)
+      .then((res) => {
+        assert.ok(msgs);
+        done();
+      });
+  });
+
+  lab.test('silent does not output', (done) => {
+
+    const oldConsole = console.log;
+    console.log = (...args) => {
+      throw new Error('not silent');
+    };
+
+    isonline(common.NODE, {silent: true, json: false})
+      .then((res) => {
+        console.log = oldConsole;
+        done();
+      });
+  });
+
+  lab.test('can output json', (done) => {
+
+    const oldConsole = console.log;
+    console.log = (...args) => {
+      assert.deepEqual({ 'http://127.0.0.1:1337': true }, args[0]);
+      return oldConsole.apply(oldConsole, args);
+    };
+
+    isonline(common.NODE, {silent: false, json: true})
+      .then((res) => {
+        console.log = oldConsole;
+        done();
+      });
+  });
+
+  lab.test('if json output is selected, colored output is not provided', (done) => {
+
+    let msgs = '';
+    log.once('log', ({message: message}) => {
+      msgs += message;
+    });
+
+    isonline(common.NODE, {silent: false, json: true})
+      .then((res) => {
+        assert.ok(!(/seems to be/).test(msgs));
+        done();
+      });
+  });
+
+  lab.test('if silent output is selected, colored output is not provided', (done) => {
+
+    const oldConsole = console.log;
+    console.log = (...args) => {
+      throw new Error('fail');
+      return oldConsole.apply(oldConsole, args);
+    };
+
+    let msgs = '';
+    log.once('log', ({message: message}) => {
+      msgs += message;
+    });
+
+    isonline(common.NODE, {silent: true, json: false})
+      .then((res) => {
+        assert.ok(!(/seems to be/).test(msgs));
+        assert.deepEqual(res, {[common.NODE]: true});
+        done();
+      });
+  });
+
+  lab.test('silent & json are not compatible', (done) => {
+
+    isonline(common.NODE, {silent: true, json: true})
+      .catch((err) => {
+        assert.ok(err instanceof Error);
+        done();
+      });
+  });
+
+});