From d9e615e69632e6876d286bbf938fdc3c9ad38da7 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Tue, 25 Mar 2014 15:33:16 +0100 Subject: [PATCH] First working version. --- packages/xo-cli/README.md | 15 +- packages/xo-cli/cli.js | 322 ++++++++++++++++++----------------- packages/xo-cli/config.js | 43 ++++- packages/xo-cli/package.json | 3 + packages/xo-cli/prompt.js | 4 +- packages/xo-cli/xo.js | 35 +++- 6 files changed, 254 insertions(+), 168 deletions(-) diff --git a/packages/xo-cli/README.md b/packages/xo-cli/README.md index aa2ed7f39..c441d30af 100644 --- a/packages/xo-cli/README.md +++ b/packages/xo-cli/README.md @@ -10,11 +10,22 @@ #### [npm](https://npmjs.org/package/xo-cli) ``` -npm install -f xo-cli +npm install -g xo-cli ``` ## Usage +#### Register your XO instance + ``` -xo-cli add-server +xo-cli register --host xo.my-company.net --email admin@admin.net --password admin +``` + +Note: only a token will be saved in the configuration file. + +#### Adds a new Xen server + + +``` +xo-cli add-server --host xen1.my-company.net --user root --password secure%password ``` diff --git a/packages/xo-cli/cli.js b/packages/xo-cli/cli.js index b93525f61..9b7657607 100644 --- a/packages/xo-cli/cli.js +++ b/packages/xo-cli/cli.js @@ -8,14 +8,171 @@ var Promise = require('bluebird'); //-------------------------------------------------------------------- +var config = require('./config'); var prompt = require('./prompt'); var Xo = require('./xo'); //==================================================================== -// Handles a promise in a CLI environment. -var handlePromise = function (promise) { - promise.then(function (value) { +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.stdout.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, + }); + }); + + // TODO: alters `fn` to prompt for each missing argument. + 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('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('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'); + }); + + // 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); @@ -23,165 +180,20 @@ var handlePromise = function (promise) { process.exit(0); }).catch(function (error) { - if (_.isString(error)) + if (error === undefined) { - console.error(error); + // Nothing to do. + undefined; } else if (_.isNumber(error)) { process.exit(error); } + else + { + console.error(error.stack || error); + } process.exit(1); }); }; - -//-------------------------------------------------------------------- - -var connect = function (opts) { - var config, xo; - - return Promise.try(function () { - opts || (opts = {}); - - // TODO: reads the configuration file. - config = { - server: 'ws://localhost:9000/api/', - }; - - if (!config.server) - { - throw 'no server to connect to!'; - } - - xo = new Xo(config.server); - - if (config.token && !opts.noAutoSignIn) - { - return xo.send('session.signInWithToken', { - token: config.token, - }); - } - }); -}; - -//-------------------------------------------------------------------- - -var addServer = function (host, username, password) { - return connect().then(function (connection) { - return connection.send('server.add', { - host: host, - username: username, - password: password, - }); - }).return('ok'); -}; - -var register = function (url, email, password) { - var xo; - return Promise.try(function () { - xo = new Xo(url); - - return xo.send('session.signInWithPassword', { - email: email, - password: password, - }); - }).then(function (user) { - console.log('Successfully logged with', user.email); - - return xo.send('token.create'); - }).then(function (token) { - return getConfig().set({ - server: url, - token: token, - }); - }); -}; - -//==================================================================== - -module.exports = function (argv) { - nomnom.command('register') - .help('signs in XO using email/password') - .options({ - url: { - help: 'URL of the API endpoint', - }, - email: { - help: 'email to use to connect', - }, - password: { - help: 'password to use to connect', - }, - }) - .callback(function (opts) { - Promise.try(function () { - if (opts.url) - { - return prompt.input('URL').then(function (url) { - opts.url = url; - }); - } - }).then(function () { - if (!opts.email) - { - return prompt.input('Email').then(function (email) { - opts.email = email; - }); - } - }).then(function () { - if (!opts.password) - { - return prompt.password('Password').then(function (password) { - opts.password = password; - }); - } - }).then(function () { - return register(opts.url, opts.email, opts.password); - }); - }) - ; - - nomnom.command('add-server') - .help('adds a new server') - .options({ - host: { - help: 'hostname or ip of the server', - }, - username: { - help: 'username to use to connect', - }, - password: { - help: 'password to use to connect', - }, - }) - .callback(function (opts) { - Promise.try(function () { - if (opts.host) - { - return prompt.input('Hostname or ip').then(function (host) { - opts.host = host; - }); - } - }).then(function () { - if (!opts.username) - { - return prompt.input('Username').then(function (username) { - opts.username = username; - }); - } - }).then(function () { - if (!opts.password) - { - return prompt.password('Password').then(function (password) { - opts.password = password; - }); - } - }).then(function () { - return addServer(opts.host, opts.username, opts.password); - }); - }) - ; - - nomnom.parse(argv); -}; diff --git a/packages/xo-cli/config.js b/packages/xo-cli/config.js index f3f4a5b9a..2bba89430 100644 --- a/packages/xo-cli/config.js +++ b/packages/xo-cli/config.js @@ -2,10 +2,47 @@ //==================================================================== -var xdg = require('xdg').basedir; +var fs = require('fs'); + +//-------------------------------------------------------------------- + +var _ = require('lodash'); +var l33t = require('l33teral'); +var mkdirp = require('mkdirp'); +var Promise = require('bluebird'); +var xdg = require('xdg'); //==================================================================== -var Config = function () { - this.data = +var configPath = xdg.basedir.configPath('xo-cli'); +var configFile = configPath +'/config.json'; + +var mkdirp = Promise.promisify(mkdirp); +var readFile = Promise.promisify(fs.readFile); +var writeFile = Promise.promisify(fs.writeFile); + +//==================================================================== + +var load = exports.load = function () { + return readFile(configFile).then(JSON.parse).catch(function () { + return {}; + }); +}; + +exports.get = function (path) { + return load().then(function (config) { + return l33t(config).tap(path); + }); +}; + +var save = exports.save = function (config) { + return mkdirp(configPath).then(function () { + return writeFile(configFile, JSON.stringify(config)); + }); +}; + +exports.set = function (data) { + return load().then(function (config) { + return save(_.extend(config, data)); + }); }; diff --git a/packages/xo-cli/package.json b/packages/xo-cli/package.json index d03bd4aae..0d1aad7aa 100644 --- a/packages/xo-cli/package.json +++ b/packages/xo-cli/package.json @@ -13,7 +13,9 @@ "bluebird": "^1.1.0", "chalk": "^0.4.0", "inquirer": "^0.4.1", + "l33teral": "^2.0.3", "lodash": "^2.4.1", + "mkdirp": "^0.3.5", "nomnom": "^1.6.2", "ws": "^0.4.31", "xdg": "^0.1.1" @@ -30,5 +32,6 @@ "bin": { "xo-cli": "bin/xo-cli" }, + "preferGlobal": true, "license": "AGPL3" } diff --git a/packages/xo-cli/prompt.js b/packages/xo-cli/prompt.js index b3499cd6c..f26e02b16 100644 --- a/packages/xo-cli/prompt.js +++ b/packages/xo-cli/prompt.js @@ -15,7 +15,7 @@ var prompts = module.exports = function (prompts) { return deferred.promise; }; -exports.input = function (message) { +prompts.input = function (message) { return prompts({ type: 'input', name: 'question', @@ -23,7 +23,7 @@ exports.input = function (message) { }).get('question'); }; -exports.password = function (message) { +prompts.password = function (message) { return prompts({ type: 'password', name: 'question', diff --git a/packages/xo-cli/xo.js b/packages/xo-cli/xo.js index 65a561523..290bdd3ab 100644 --- a/packages/xo-cli/xo.js +++ b/packages/xo-cli/xo.js @@ -8,7 +8,9 @@ var Promise = require('bluebird'); // Supports browsers. // FIXME: wraps in an anonymous function. -var WebSocket = 'WebSocket' in window ? window.WebSocket : require('ws'); +// jshint ignore: start +var WebSocket = (this && 'WebSocket' in this) ? this.WebSocket : require('ws'); +// jshint ignore: end //==================================================================== @@ -32,6 +34,13 @@ var Xo = function (url) { }; _.extend(Xo.prototype, { + close: function () { + if (this._socket) + { + this._socket.close(); + } + }, + connect: function () { if (this.status === 'connected') { @@ -48,13 +57,22 @@ _.extend(Xo.prototype, { socket.on('open', function () { this.status = 'connected'; - // Reopens accesses. + // (Re)Opens accesses. delete this.send; + + // Resolves the promise. + deferred.resolve(); }.bind(this)); - socket.on('message', function (event) { + socket.on('message', function (data) { + // `ws` API is lightly different from standard API. + if (data.data) + { + data = data.data; + } + // TODO: Wraps in a promise to prevent releasing the Zalgo. - var response = JSON.parse(event.data); + var response = JSON.parse(data); var id = response.id; @@ -80,7 +98,7 @@ _.extend(Xo.prototype, { message: 'invalid response received', object: response, }); - }); + }.bind(this)); socket.on('close', function () { // Closes accesses. @@ -95,6 +113,11 @@ _.extend(Xo.prototype, { this._deferreds = {}; }.bind(this)); + socket.on('error', function (error) { + // Fails the connect promise if possible. + deferred.reject(error); + }); + return deferred.promise; }, @@ -114,7 +137,7 @@ _.extend(Xo.prototype, { var deferred = this._deferreds[id] = Promise.defer(); return deferred.promise; - }); + }.bind(this)); }, });