chore(xo-cli): use modern ES

This commit is contained in:
Julien Fontanet 2017-10-23 11:14:46 +02:00
parent 37f0bffcaa
commit 50581c539c
10 changed files with 3641 additions and 548 deletions

View File

@ -0,0 +1,22 @@
const { NODE_ENV = 'development' } = process.env
module.exports = {
comments: false,
compact: true,
ignore: NODE_ENV === 'test' ? undefined : ['*.spec.js'],
// plugins: ['lodash']
presets: [
[
'env',
{
debug: true,
loose: true,
targets: {
node: process.env.NODE_ENV === 'production' ? '6' : 'current'
},
useBuiltIns: 'usage'
}
],
'flow'
]
}

View File

@ -0,0 +1,12 @@
[ignore]
[include]
[libs]
[lints]
[options]
experimental.const_params=true
module.use_strict=true
unsafe.enable_getters_and_setters=true

View File

@ -0,0 +1,24 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@ -0,0 +1,8 @@
language: node_js
node_js:
- stable
- 6
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false

View File

@ -5,7 +5,7 @@
> Basic CLI for Xen-Orchestra
## Installation
## Install
#### [npm](https://npmjs.org/package/xo-cli)
@ -117,9 +117,31 @@ encoding by prefixing with `json:`:
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
## Contributing
## Development
Contributions are *very* welcome, either on the documentation or on
```
# Install dependencies
> yarn
# Run the tests
> yarn test
# Continuously compile
> yarn dev
# Continuously run the tests
> yarn dev-test
# Build for production (automatically called by npm install)
> yarn build
# Commit changes
> yarn cz
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:

View File

@ -1,432 +0,0 @@
#!/usr/bin/env node
'use strict'
var Bluebird = require('bluebird')
Bluebird.longStackTraces()
var createReadStream = require('fs').createReadStream
var createWriteStream = require('fs').createWriteStream
var resolveUrl = require('url').resolve
var stat = require('fs-promise').stat
var chalk = require('chalk')
var eventToPromise = require('event-to-promise')
var forEach = require('lodash/forEach')
var getKeys = require('lodash/keys')
var got = require('got')
var humanFormat = require('human-format')
var identity = require('lodash/identity')
var isArray = require('lodash/isArray')
var isObject = require('lodash/isObject')
var micromatch = require('micromatch')
var multiline = require('multiline')
var nicePipe = require('nice-pipe')
var pairs = require('lodash/toPairs')
var pick = require('lodash/pick')
var prettyMs = require('pretty-ms')
var progressStream = require('progress-stream')
var pw = require('pw')
var Xo = require('xo-lib').default
// -------------------------------------------------------------------
var config = require('./config')
// ===================================================================
function connect () {
return config.load().bind({}).then(function (config) {
if (!config.server) {
throw new Error('no server to connect to!')
}
if (!config.token) {
throw new Error('no token available')
}
var xo = new Xo({ url: config.server })
return xo.open().then(function () {
return xo.signIn({ token: config.token })
}).then(function () {
return xo
})
})
}
function _startsWith (string, search) {
return string.lastIndexOf(search, 0) === 0
}
var FLAG_RE = /^--([^=]+)(?:=([^]*))?$/
function extractFlags (args) {
var flags = {}
var i = 0
var n = args.length
var matches
while (i < n && (matches = args[i].match(FLAG_RE))) {
var value = matches[2]
flags[matches[1]] = value === undefined ? true : value
++i
}
args.splice(0, i)
return flags
}
var PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters (args) {
var params = {}
forEach(args, function (arg) {
var matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
}
var name = matches[1]
var value = matches[2]
if (_startsWith(value, 'json:')) {
value = JSON.parse(value.slice(5))
}
if (name === '@') {
params['@'] = value
return
}
if (value === 'true') {
value = true
} else if (value === 'false') {
value = false
}
params[name] = value
})
return params
}
var humanFormatOpts = {
unit: 'B',
scale: 'binary'
}
function printProgress (progress) {
if (progress.length) {
console.warn('%s% of %s @ %s/s - ETA %s',
Math.round(progress.percentage),
humanFormat(progress.length, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts),
prettyMs(progress.eta * 1e3)
)
} else {
console.warn('%s @ %s/s',
humanFormat(progress.transferred, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts)
)
}
}
function wrap (val) {
return function wrappedValue () {
return val
}
}
// ===================================================================
var help = wrap((function (pkg) {
return multiline.stripIndent(function () { /*
Usage:
$name --register [--expiresIn duration] <XO-Server URL> <username> [<password>]
Registers the XO instance to use.
--expiresIn duration
Can be used to change the validity duration of the
authorization token (default: one month).
$name --unregister
Remove stored credentials.
$name --list-commands [--json] [<pattern>]...
Returns the list of available commands on the current XO instance.
The patterns can be used to filter on command names.
$name --list-objects [--<property>] [<property>=<value>]...
Returns a list of XO objects.
--<property>
Restricts displayed properties to those listed.
<property>=<value>
Restricted displayed objects to those matching the patterns.
$name <command> [<name>=<value>]...
Executes a command on the current XO instance.
$name v$version
*/ }).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
if (arg) {
return '<' + chalk.yellow(arg) + '>'
}
if (key === 'name') {
return chalk.bold(pkg[key])
}
return pkg[key]
})
})(require('./package')))
// -------------------------------------------------------------------
function main (args) {
if (!args || !args.length || args[0] === '-h') {
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)
}
exports = module.exports = main
// -------------------------------------------------------------------
exports.help = help
function register (args) {
var i = 0
var expiresIn
if (args[i] === '--expiresIn') {
expiresIn = args[i + 1]
i += 2
}
var url = args[i++]
var email = args[i++]
var password = args[i++]
var xo
return Promise.resolve(password || new Promise(function (resolve) {
process.stdout.write('Password: ')
pw(resolve)
})).then(function (password_) {
password = password_
xo = new Xo({ url })
return xo.open()
}).then(function () {
return xo.signIn({
email: email,
password: password
})
}).then(function () {
console.log('Successfully logged with', xo.user.email)
return xo.call('token.create', { expiresIn: expiresIn })
}).then(function (token) {
return config.set({
server: url,
token: token
})
})
}
exports.register = register
function unregister () {
return config.unset([
'server',
'token'
])
}
exports.unregister = unregister
function listCommands (args) {
return connect().then(function getMethodsInfo (xo) {
return xo.call('system.getMethodsInfo')
}).then(function formatMethodsInfo (methods) {
var json = false
var patterns = []
forEach(args, function (arg) {
if (arg === '--json') {
json = true
} else {
patterns.push(arg)
}
})
if (patterns.length) {
methods = pick(methods, micromatch(Object.keys(methods), patterns))
}
if (json) {
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 = []
forEach(methods, function (method) {
var name = method[0]
var info = method[1]
str.push(chalk.bold.blue(name))
forEach(info.params || [], function (info, name) {
str.push(' ')
if (info.optional) {
str.push('[')
}
var type = info.type
str.push(
name,
'=<',
type == null
? 'unknown type'
: isArray(type)
? type.join('|')
: type,
'>'
)
if (info.optional) {
str.push(']')
}
})
str.push('\n')
if (info.description) {
str.push(' ', info.description, '\n')
}
})
return str.join('')
})
}
exports.listCommands = listCommands
function listObjects (args) {
var properties = getKeys(extractFlags(args))
var filterProperties = properties.length
? function (object) {
return pick(object, properties)
}
: identity
var sieve = args.length ? parseParameters(args) : undefined
return connect().then(function getXoObjects (xo) {
return xo.call('xo.getAllObjects', { filter: sieve })
}).then(function filterObjects (objects) {
const stdout = process.stdout
stdout.write('[\n')
const keys = Object.keys(objects)
for (var i = 0, n = keys.length; i < n;) {
stdout.write(JSON.stringify(filterProperties(objects[keys[i]]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
stdout.write(']\n')
})
}
exports.listObjects = listObjects
function call (args) {
if (!args.length) {
throw new Error('missing command name')
}
var method = args.shift()
var params = parseParameters(args)
var file = params['@']
delete params['@']
var baseUrl
return connect().then(function (xo) {
// FIXME: do not use private properties.
baseUrl = xo._url.replace(/^ws/, 'http')
return xo.call(method, params)
}).then(function handleResult (result) {
var keys, key, url
if (
isObject(result) &&
(keys = getKeys(result)).length === 1
) {
key = keys[0]
if (key === '$getFrom') {
url = resolveUrl(baseUrl, result[key])
var output = createWriteStream(file)
var progress = progressStream({ time: 1e3 }, printProgress)
return eventToPromise(nicePipe([
got.stream(url).on('response', function (response) {
var length = response.headers['content-length']
if (length) {
progress.length(length)
}
}),
progress,
output
]), 'finish')
}
if (key === '$sendTo') {
url = resolveUrl(baseUrl, result[key])
return stat(file).then(function (stats) {
var length = stats.size
var input = nicePipe([
createReadStream(file),
progressStream({
length: length,
time: 1e3
}, printProgress)
])
return got.post(url, {
body: input,
headers: {
'content-length': length
},
method: 'POST'
}).then(function (response) {
return response.body
})
})
}
}
return result
})
}
exports.call = call
// ===================================================================
if (!module.parent) {
require('exec-promise')(exports)
}

View File

@ -11,21 +11,26 @@
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-cli",
"bugs": "https://github.com/vatesfr/xo-web/issues",
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
"preferGlobal": true,
"bin": {
"xo-cli": "index.js"
},
"files": [
"*.js"
],
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
"preferGlobal": true,
"main": "dist/",
"bin": {
"xo-cli": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=6"
},
"dependencies": {
"babel-polyfill": "^7.0.0-beta.3",
"bluebird": "^3.5.1",
"chalk": "^2.1.0",
"chalk": "^2.2.0",
"event-to-promise": "^0.8.0",
"exec-promise": "^0.7.0",
"fs-promise": "^2.0.3",
@ -33,25 +38,60 @@
"human-format": "^0.8.0",
"l33teral": "^3.0.3",
"lodash": "^4.17.4",
"micromatch": "^3.1.0",
"micromatch": "^3.1.3",
"mkdirp": "^0.5.1",
"multiline": "^1.0.2",
"nice-pipe": "0.0.0",
"pretty-ms": "^3.0.1",
"progress-stream": "^2.0.0",
"pw": "^0.0.4",
"strip-indent": "^2.0.0",
"xdg-basedir": "^3.0.0",
"xo-lib": "^0.9.0"
},
"devDependencies": {
"babel-cli": "^7.0.0-beta.3",
"babel-eslint": "^8.0.1",
"babel-plugin-lodash": "^3.2.11",
"babel-preset-env": "^7.0.0-beta.3",
"babel-preset-flow": "^7.0.0-beta.3",
"commitizen": "^2.9.6",
"cross-env": "^5.1.0",
"cz-conventional-changelog": "^2.0.0",
"dependency-check": "^2.9.1",
"flow-bin": "^0.57.3",
"jest": "^21.2.1",
"rimraf": "^2.6.2",
"standard": "^10.0.3"
},
"scripts": {
"test": "standard"
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"commitmsg": "npm test",
"cz": "git-cz",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"dev-test": "jest --bail --watch",
"posttest": "dependency-check ./package.json",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublish": "npm run build",
"pretest": "standard --fix && flow status",
"test": "jest"
},
"greenkeeper": {
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"jest": {
"testEnvironment": "node",
"roots": [
"<rootDir>/src"
],
"testRegex": "\\.spec\\.js$"
},
"standard": {
"ignore": [
"nice-pipe"
]
"dist"
],
"parser": "babel-eslint"
}
}

407
packages/xo-cli/src/index.js Executable file
View File

@ -0,0 +1,407 @@
#!/usr/bin/env node
'use strict'
const Bluebird = require('bluebird')
Bluebird.longStackTraces()
const createReadStream = require('fs').createReadStream
const createWriteStream = require('fs').createWriteStream
const resolveUrl = require('url').resolve
const stat = require('fs-promise').stat
const chalk = require('chalk')
const eventToPromise = require('event-to-promise')
const forEach = require('lodash/forEach')
const getKeys = require('lodash/keys')
const got = require('got')
const humanFormat = require('human-format')
const identity = require('lodash/identity')
const isArray = require('lodash/isArray')
const isObject = require('lodash/isObject')
const micromatch = require('micromatch')
const nicePipe = require('nice-pipe')
const pairs = require('lodash/toPairs')
const pick = require('lodash/pick')
const startsWith = require('lodash/startsWith')
const prettyMs = require('pretty-ms')
const progressStream = require('progress-stream')
const pw = require('pw')
const Xo = require('xo-lib').default
// -------------------------------------------------------------------
const config = require('./config')
// ===================================================================
async function connect () {
const { server, token } = await config.load()
if (server === undefined) {
throw new Error('no server to connect to!')
}
if (token === undefined) {
throw new Error('no token available')
}
const xo = new Xo({ url: server })
await xo.open()
await xo.signIn({ token })
return xo
}
const FLAG_RE = /^--([^=]+)(?:=([^]*))?$/
function extractFlags (args) {
const flags = {}
let i = 0
const n = args.length
let matches
while (i < n && (matches = args[i].match(FLAG_RE))) {
const value = matches[2]
flags[matches[1]] = value === undefined ? true : value
++i
}
args.splice(0, i)
return flags
}
const PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters (args) {
const params = {}
forEach(args, function (arg) {
let matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
}
const name = matches[1]
let value = matches[2]
if (startsWith(value, 'json:')) {
value = JSON.parse(value.slice(5))
}
if (name === '@') {
params['@'] = value
return
}
if (value === 'true') {
value = true
} else if (value === 'false') {
value = false
}
params[name] = value
})
return params
}
const humanFormatOpts = {
unit: 'B',
scale: 'binary'
}
function printProgress (progress) {
if (progress.length) {
console.warn('%s% of %s @ %s/s - ETA %s',
Math.round(progress.percentage),
humanFormat(progress.length, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts),
prettyMs(progress.eta * 1e3)
)
} else {
console.warn('%s @ %s/s',
humanFormat(progress.transferred, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts)
)
}
}
function wrap (val) {
return function wrappedValue () {
return val
}
}
// ===================================================================
const help = wrap((function (pkg) {
return require('strip-indent')(`
Usage:
$name --register [--expiresIn duration] <XO-Server URL> <username> [<password>]
Registers the XO instance to use.
--expiresIn duration
Can be used to change the validity duration of the
authorization token (default: one month).
$name --unregister
Remove stored credentials.
$name --list-commands [--json] [<pattern>]...
Returns the list of available commands on the current XO instance.
The patterns can be used to filter on command names.
$name --list-objects [--<property>] [<property>=<value>]...
Returns a list of XO objects.
--<property>
Restricts displayed properties to those listed.
<property>=<value>
Restricted displayed objects to those matching the patterns.
$name <command> [<name>=<value>]...
Executes a command on the current XO instance.
$name v$version
`).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
if (arg) {
return '<' + chalk.yellow(arg) + '>'
}
if (key === 'name') {
return chalk.bold(pkg[key])
}
return pkg[key]
})
})(require('../package')))
// -------------------------------------------------------------------
function main (args) {
if (!args || !args.length || args[0] === '-h') {
return help()
}
const 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)
}
exports = module.exports = main
// -------------------------------------------------------------------
exports.help = help
async function register (args) {
let expiresIn
if (args[0] === '--expiresIn') {
expiresIn = args[1]
args = args.slice(2)
}
const [
url,
email,
password = await new Promise(function (resolve) {
process.stdout.write('Password: ')
pw(resolve)
})
] = args
const xo = new Xo({ url })
await xo.open()
await xo.signIn({ email, password })
console.log('Successfully logged with', xo.user.email)
await config.set({
server: url,
token: await xo.call('token.create', { expiresIn })
})
}
exports.register = register
function unregister () {
return config.unset([
'server',
'token'
])
}
exports.unregister = unregister
async function listCommands (args) {
const xo = await connect()
let methods = await xo.call('system.getMethodsInfo')
let json = false
const patterns = []
forEach(args, function (arg) {
if (arg === '--json') {
json = true
} else {
patterns.push(arg)
}
})
if (patterns.length) {
methods = pick(methods, micromatch(Object.keys(methods), patterns))
}
if (json) {
return methods
}
methods = pairs(methods)
methods.sort(function (a, b) {
a = a[0]
b = b[0]
if (a < b) {
return -1
}
return +(a > b)
})
const str = []
forEach(methods, function (method) {
const name = method[0]
const info = method[1]
str.push(chalk.bold.blue(name))
forEach(info.params || [], function (info, name) {
str.push(' ')
if (info.optional) {
str.push('[')
}
const type = info.type
str.push(
name,
'=<',
type == null
? 'unknown type'
: isArray(type)
? type.join('|')
: type,
'>'
)
if (info.optional) {
str.push(']')
}
})
str.push('\n')
if (info.description) {
str.push(' ', info.description, '\n')
}
})
return str.join('')
}
exports.listCommands = listCommands
async function listObjects (args) {
const properties = getKeys(extractFlags(args))
const filterProperties = properties.length
? function (object) {
return pick(object, properties)
}
: identity
const filter = args.length ? parseParameters(args) : undefined
const xo = await connect()
const objects = await xo.call('xo.getAllObjects', { filter })
const stdout = process.stdout
stdout.write('[\n')
const keys = Object.keys(objects)
for (let i = 0, n = keys.length; i < n;) {
stdout.write(JSON.stringify(filterProperties(objects[keys[i]]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
stdout.write(']\n')
}
exports.listObjects = listObjects
async function call (args) {
if (!args.length) {
throw new Error('missing command name')
}
const method = args.shift()
const params = parseParameters(args)
const file = params['@']
delete params['@']
const xo = await connect()
// FIXME: do not use private properties.
const baseUrl = xo._url.replace(/^ws/, 'http')
const result = await xo.call(method, params)
let keys, key, url
if (
isObject(result) &&
(keys = getKeys(result)).length === 1
) {
key = keys[0]
if (key === '$getFrom') {
url = resolveUrl(baseUrl, result[key])
const output = createWriteStream(file)
const progress = progressStream({ time: 1e3 }, printProgress)
return eventToPromise(nicePipe([
got.stream(url).on('response', function (response) {
const length = response.headers['content-length']
if (length !== undefined) {
progress.length(length)
}
}),
progress,
output
]), 'finish')
}
if (key === '$sendTo') {
url = resolveUrl(baseUrl, result[key])
const stats = await stat(file)
const length = stats.size
const input = nicePipe([
createReadStream(file),
progressStream({
length: length,
time: 1e3
}, printProgress)
])
const response = await got.post(url, {
body: input,
headers: {
'content-length': length
},
method: 'POST'
})
return response.body
}
}
return result
}
exports.call = call
// ===================================================================
if (!module.parent) {
require('exec-promise')(exports)
}

File diff suppressed because it is too large Load Diff