diff --git a/packages/xo-cli/README.md b/packages/xo-cli/README.md index ac3ecdf9a..68b1059f3 100644 --- a/packages/xo-cli/README.md +++ b/packages/xo-cli/README.md @@ -1,4 +1,4 @@ -# XO CLI +# XO-CLI [![Build Status](https://img.shields.io/travis/vatesfr/xo-cli/master.svg)](http://travis-ci.org/vatesfr/xo-cli) [![Dependency Status](https://david-dm.org/vatesfr/xo-cli/status.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-cli) [![devDependency Status](https://david-dm.org/vatesfr/xo-cli/dev-status.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-cli#info=devDependencies) @@ -18,14 +18,43 @@ npm install -g xo-cli #### Register your XO instance ``` -xo-cli register --host http://xo.my-company.net/api/ --email admin@admin.net --password admin +xo-cli --register http://xo.my-company.net/api/admin@admin.net admin ``` Note: only a token will be saved in the configuration file. -#### Adds a new Xen server - +#### List available commands ``` -xo-cli add-server --host xen1.my-company.net --user root --password secure%password +xo-cli --list-commands ``` + +#### Execute a command + +The same syntax is used for all commands: `xo-cli =...` + +E.g., adding a new server: + +``` +xo-cli server.add my.server.net root secret-password +42 +``` + +The return value is the identifier of this new server in XO. + +## Contributing + +Contributions are *very* welcome, either on the documentation or on +the code. + +You may: + +- report any [issue](https://github.com/vatesfr/xo-cli/issues) + you've encountered; +- fork and create a pull request. + +## License + +XO-CLI is released under the [AGPL +v3](http://www.gnu.org/licenses/agpl-3.0-standalone.html). diff --git a/packages/xo-cli/bin/xo-cli b/packages/xo-cli/bin/xo-cli index 77127e56d..bfe13c7d4 100755 --- a/packages/xo-cli/bin/xo-cli +++ b/packages/xo-cli/bin/xo-cli @@ -4,4 +4,4 @@ //==================================================================== -require('../')(process.argv.slice(2)); +require('exec-promise')(require('..')); diff --git a/packages/xo-cli/cli.js b/packages/xo-cli/cli.js deleted file mode 100644 index 502e1d945..000000000 --- a/packages/xo-cli/cli.js +++ /dev/null @@ -1,216 +0,0 @@ -'use strict'; - -//==================================================================== - -var _ = require('lodash'); -var nomnom = require('nomnom'); -var Promise = require('bluebird'); - -//-------------------------------------------------------------------- - -var config = require('./config'); -var prompt = require('./prompt'); -var Xo = require('./xo'); - -//==================================================================== - -var connect = function () { - return config.load().bind({}).then(function (config) { - if (!config.server) - { - throw 'no server to connect to!'; - } - - if (!config.token) - { - throw 'no token available'; - } - - this.xo = new Xo(config.server); - - return this.xo.send('session.signInWithToken', { - token: config.token, - }); - }).then(function () { - return this.xo; - }).bind(); -}; - -//==================================================================== - -module.exports = function (argv) { - var command; - var commandName; - var commandOpts; - - var registerCommand = function (name, def, fn) { - var cmdParser = nomnom.command(name); - - if (def.description) - { - cmdParser.help(def.description); - } - - if (def.args) - { - var interactive = process.stdin.isTTY; - - _.each(def.args, function (def, name) { - cmdParser.option(name, { - abbr: def.abbr, - flag: (def.type === 'boolean'), - help: def.description, - - // Do not mark options as required if the program is run in - // interactive mode, they will be asked. - required: !interactive && def.required, - }); - }); - - fn = (function (fn, args) { - return function (opts) { - var prompts = []; - - _.each(args, function (def, name) { - if (!(name in opts)) - { - prompts.push({ - name: name, - message: def.prompt || def.description, - type: def.promptType || 'input', - }); - } - }); - - if (prompts.length) - { - return prompt(prompts).then(function (answers) { - return fn(_.extend(opts, answers)); - }); - } - - return fn(opts); - }; - })(fn, def.args); - } - - cmdParser.callback(function (opts) { - command = fn; - commandName = name; - commandOpts = opts; - }); - }; - - registerCommand('add-server', { - description: 'adds a new Xen server', - args: { - host: { - description: 'host of the server', - required: true, - }, - username: { - description: 'username to use to connect', - required: true, - }, - password: { - description: 'password to use to connect', - promptType: 'password', - required: true, - }, - }, - }, function (opts) { - return connect().then(function (connection) { - return connection.send('server.add', { - host: opts.host, - username: opts.username, - password: opts.password, - }); - }).return('ok'); - }); - - registerCommand('register', { - description: 'registers the XO instance', - args: { - host: { - description: 'host/ip optionally followed by `:port` if not 80', - required: true, - }, - email: { - description: 'email to use to connect', - required: true, - }, - password: { - description: 'password to use to connect', - promptType: 'password', - required: true, - }, - } - }, function (opts) { - return Promise.bind({opts: opts || {}}).then(function () { - this.xo = new Xo(opts.host); - - return this.xo.send('session.signInWithPassword', { - email: this.opts.email, - password: this.opts.password, - }); - }).then(function (user) { - console.log('Successfully logged with', user.email); - - return this.xo.send('token.create'); - }).then(function (token) { - console.log('Token created:', token); - - return config.set({ - server: this.opts.host, - token: token, - }); - }).bind(); - }); - - registerCommand('whoami', { - description: 'displays information about the current user', - }, function () { - return connect().then(function (xo) { - return xo.send('session.getUser'); - }).then(function (user) { - if (user) - { - console.log('You are signed in as', user.email); - console.log('Your global permission is', user.permission); - } - else - { - console.log('Your are not signed in.'); - } - }); - }); - - // TODO: handle global `--config FILE` option. - nomnom.parse(argv); - - // Executes the selected command. - Promise.try(command, [commandOpts]).then(function (value) { - if (_.isString(value)) - { - console.log(value); - } - - process.exit(0); - }).catch(function (error) { - if (error === undefined) - { - // Nothing to do. - undefined; - } - else if (_.isNumber(error)) - { - process.exit(error); - } - else - { - console.error(error.stack || error); - } - - process.exit(1); - }); -}; diff --git a/packages/xo-cli/package.json b/packages/xo-cli/package.json index 1424c01ee..307808081 100644 --- a/packages/xo-cli/package.json +++ b/packages/xo-cli/package.json @@ -1,38 +1,44 @@ { "name": "xo-cli", "version": "0.1.1", + "license": "AGPL3", "description": "Basic CLI for Xen-Orchestra", - "author": "Julien Fontanet ", + "keywords": [ + "xo", + "xen-orchestra", + "xen", + "orchestra" + ], "homepage": "https://github.com/vatesfr/xo-cli", - "bugs": { - "url": "https://github.com/vatesfr/xo-cli/issues" + "bugs": "https://github.com/vatesfr/xo-cli/issues", + "author": "Julien Fontanet ", + "preferGlobal": true, + "main": "./src/cli.js", + "directories": { + "bin": "./bin", + "man": "./man" }, "repository": { "type": "git", "url": "https://github.com/vatesfr/xo-cli" }, "dependencies": { - "bluebird": "^1.2.2", + "bluebird": "^1.2.4", "chalk": "^0.4.0", - "inquirer": "^0.4.1", + "exec-promise": "^0.2.4", + "inquirer": "^0.5.0", "l33teral": "^2.0.4", "lodash": "^2.4.1", - "mkdirp": "^0.3.5", - "nomnom": "^1.6.2", + "mkdirp": "^0.5.0", + "multiline": "^0.3.4", "ws": "^0.4.31", "xdg": "^0.1.1" }, "devDependencies": { "chai": "^1.9.1", - "mocha": "^1.18.2" + "mocha": "^1.19.0" }, "scripts": { "test": "mocha cli.spec.js" - }, - "main": "cli.js", - "directories": { - "bin": "./bin" - }, - "preferGlobal": true, - "license": "AGPL3" + } } diff --git a/packages/xo-cli/src/cli.js b/packages/xo-cli/src/cli.js new file mode 100644 index 000000000..397939ed4 --- /dev/null +++ b/packages/xo-cli/src/cli.js @@ -0,0 +1,173 @@ +'use strict'; + +//==================================================================== + +var _ = require('lodash'); +var Promise = require('bluebird'); +var multiline = require('multiline'); +var chalk = require('chalk'); + +//-------------------------------------------------------------------- + +var config = require('./config'); +var prompt = require('./prompt'); +var Xo = require('./xo'); + +//==================================================================== + +var connect = function () { + return config.load().bind({}).then(function (config) { + if (!config.server) + { + throw 'no server to connect to!'; + } + + if (!config.token) + { + throw 'no token available'; + } + + var xo = new Xo(config.server); + + return xo.call('session.signInWithToken', { + token: config.token, + }).return(xo); + }); +}; + +var wrap = function (val) { + return function () { + return val; + }; +}; + +//==================================================================== + +exports = module.exports = function (args) { + if (!args || !args.length) { + return help(); + } + + var fnName = args[0].replace(/^--|-\w/g, function (match) { + if (match === '--') + { + return ''; + } + + return match[1].toUpperCase(); + }); + if (fnName in exports) { + return exports[fnName](args.slice(1)); + } + + return exports.call(args); +}; + +//-------------------------------------------------------------------- + +var help = exports.help = wrap(multiline.stripIndent(function () {/* + Usage: + + xo-cli --register [] [] [] + Registers the XO instance to use. + + xo-cli --list-commands [--json] + Returns the list of available commands on the current XO instance. + + xo-cli [=]... + Executes a command on the current XO instance. +*/})); + +exports.version = wrap('xo-cli v'+ require('../package').version); + +exports.register = function (args) { + var xo; + return Promise.try(function () { + xo = new Xo(args[0]); + + return xo.call('session.signInWithPassword', { + email: args[1], + password: args[2], + }); + }).then(function (user) { + console.log('Successfully logged with', user.email); + + return xo.call('token.create'); + }).then(function (token) { + return config.set({ + server: args[0], + token: token, + }); + }); +}; + +exports.unregister = function () { + return config.unset([ + 'server', + 'token', + ]); +} + +exports.listCommands = function (args) { + return connect().then(function (xo) { + return xo.call('system.getMethodsInfo'); + }).then(function (methods) { + if (args.indexOf('--json') !== -1) + { + return methods; + } + + methods = _.pairs(methods); + methods.sort(function (a, b) { + a = a[0]; + b = b[0]; + if (a < b) { + return -1; + } + return +(a > b); + }); + + var str = []; + methods.forEach(function (method) { + var name = method[0]; + var info = method[1]; + str.push(chalk.bold.blue(name)); + _.each(info.params || [], function (info, name) { + str.push(' '); + if (info.optional) { + str.push('['); + } + str.push(name, '=<', info.type || 'unknown', '>'); + if (info.optional) { + str.push(']'); + } + }); + str.push('\n'); + if (info.description) { + str.push(' ', info.description, '\n'); + } + }); + return str.join(''); + }); +}; + +var PARAM_RE = /^([^=]+)=(.*)$/; +exports.call = function (args) { + if (!args.length) { + throw 'missing command name'; + } + + var method = args.shift(); + var params = {}; + args.forEach(function (arg) { + var matches; + if (!(matches = arg.match(PARAM_RE))) { + throw 'invalid arg: '+arg; + } + params[matches[1]] = matches[2]; + }); + + return connect().then(function (xo) { + return xo.call(method, params); + }); +}; diff --git a/packages/xo-cli/config.js b/packages/xo-cli/src/config.js similarity index 84% rename from packages/xo-cli/config.js rename to packages/xo-cli/src/config.js index 2bba89430..47129eade 100644 --- a/packages/xo-cli/config.js +++ b/packages/xo-cli/src/config.js @@ -46,3 +46,13 @@ exports.set = function (data) { return save(_.extend(config, data)); }); }; + +exports.unset = function (paths) { + return load().then(function (config) { + var l33tConfig = l33t(config); + [].concat(paths).forEach(function (path) { + l33tConfig.purge(path, true); + }); + return save(config); + }); +}; diff --git a/packages/xo-cli/prompt.js b/packages/xo-cli/src/prompt.js similarity index 100% rename from packages/xo-cli/prompt.js rename to packages/xo-cli/src/prompt.js diff --git a/packages/xo-cli/xo.js b/packages/xo-cli/src/xo.js similarity index 93% rename from packages/xo-cli/xo.js rename to packages/xo-cli/src/xo.js index 290bdd3ab..678a68689 100644 --- a/packages/xo-cli/xo.js +++ b/packages/xo-cli/src/xo.js @@ -14,6 +14,12 @@ var WebSocket = (this && 'WebSocket' in this) ? this.WebSocket : require('ws'); //==================================================================== +var notConnected = function () { + throw new Error('not connected'); +}; + +//==================================================================== + var Xo = function (url) { this._url = url; @@ -102,9 +108,7 @@ _.extend(Xo.prototype, { socket.on('close', function () { // Closes accesses. - this.send = function () { - throw new Error('not connected'); - }; + this.send = notConnected; // Fails all waiting requests. _.each(this._deferreds, function (deferred) { @@ -121,7 +125,7 @@ _.extend(Xo.prototype, { return deferred.promise; }, - send: function (method, params) { + call: function (method, params) { return this.connect().then(function () { var socket = this._socket;