diff --git a/lib/command/flag-to-prompt.js b/lib/command/flag-to-prompt.js new file mode 100644 index 0000000..df4a8b9 --- /dev/null +++ b/lib/command/flag-to-prompt.js @@ -0,0 +1,19 @@ +'use strict' + +const flagType = require('./flag-type') + +module.exports = flagToPrompt + +function flagToPrompt(name, opt) { + const display = name.replace(':', ' ') + return { + 'type': flagType(opt) + , 'name': name + , 'message': display + ': ' + (opt.description || '(no description)') + , 'choices': opt.choices + , 'default': opt.default || null + , 'when': opt.when + , 'validate': opt.validate + , 'filter': opt.filter + } +} diff --git a/lib/command/flag-type.js b/lib/command/flag-type.js new file mode 100644 index 0000000..a235b60 --- /dev/null +++ b/lib/command/flag-type.js @@ -0,0 +1,24 @@ +'use strict' + +module.exports = flagType + +function flagType(flag) { + if (Array.isArray(flag.type)) return flagType(unnest(flag)) + if (flag.type === Boolean) return 'confirm' + if (flag.type === Number) return 'number' + if (flag.mask) return 'password' + if (flag.choices) { + if (flag.multi) return 'checkbox' + return 'list' + } + + return 'input' +} + +function unnest(flag) { + const types = flag.type.filter((type) => { + return type !== Array + }) + + return {type: types[0]} +} diff --git a/lib/command.js b/lib/command/index.js similarity index 92% rename from lib/command.js rename to lib/command/index.js index 5c9138f..3a7de77 100644 --- a/lib/command.js +++ b/lib/command/index.js @@ -31,12 +31,13 @@ const ora = require('ora') const toArray = require('mout/lang/toArray') const isFunction = require('mout/lang/isFunction') const hasOwn = require('mout/object/hasOwn') -const Registry = require('./registry') -const conf = require('./conf') -const usage = require('./usage') -const object = require('./lang/object') -const typeOf = require('./usage/type-of') -const exceptions = require('./exceptions') +const toPrompt = require('./flag-to-prompt') +const Registry = require('../registry') +const conf = require('../conf') +const usage = require('../usage') +const object = require('../lang/object') +const typeOf = require('../usage/type-of') +const exceptions = require('../exceptions') const stop_flags = new Set(['help', 'interactive', 'skip', 'color']) const ARGV = 'argv' const on_exp = /^on([A-z])/ @@ -245,9 +246,7 @@ class Command extends Registry { **/ get conf() { - if (this._optcache) { - return this._optcache - } + if (this._optcache) return this._optcache this._optcache = {} this._required = new Set() @@ -350,7 +349,7 @@ class Command extends Registry { answers[flag] = await ask(flag, cmd, current, this[kPrompt]) continue } - const arg = toQuestion(flag, cmd, current, answers) + const arg = toQuestion(flag, current, answers) const res = await this.prompt(arg) Object.assign(answers, res) } @@ -499,21 +498,44 @@ class Command extends Registry { return (this.argv.color && chalk[color]) ? chalk[color](text) : text } + /** + * Registers a new sub command + * @method module:seeli/lib/command#use + * @param {module:seeli/lib/command} command The command to register + **/ use(...args) { return this.register(...args) } + /** + * Convert all registered flags to inquierer compatible prompt objects + * @method module:seeli/lib/command#toPrompt + * @returns {Object[]} array of inquirer prompt objects + **/ + toPrompt() { + const prompts = [] + const flags = this.options.flags + for (const [name, opts] of Object.entries(flags)) { + if (stop_flags.has(name)) continue + if (opts.interactive === false) continue + prompts.push(toPrompt(name, opts)) + } + return prompts + } + static run(...args) { const cmd = new(this)() return cmd.run(...args) } + } module.exports = Command -async function ask(name, cmd, opts, inquirer) { +/* istanbul ignore next */ +async function ask(name, opts, inquirer) { const results = [] - const question = toQuestion(name, cmd, opts) + const question = toQuestion(name, opts) while (true) { const answer = await inquirer(question) if (answer[name] === '') break @@ -522,14 +544,8 @@ async function ask(name, cmd, opts, inquirer) { return results } -function toQuestion(flag, cmd, opts, answers) { - const flag_display = flag.replace(':', ' ') - const arg = { - 'type': opts.type === Boolean ? 'confirm' : opts.mask ? 'password' : 'input' - , 'name': flag - , 'message': flag_display + ': ' + (opts.description || '(no description)') - , 'default': opts.default || null - } +function toQuestion(flag, opts, answers) { + const arg = toPrompt(flag, opts) // TODO(esatterwhite) // wrap validate to throw returned errors so `ask` @@ -537,12 +553,6 @@ function toQuestion(flag, cmd, opts, answers) { arg.when = opts.when ? opts.when.bind(null, answers) : undefined arg.validate = opts.validate ? opts.validate.bind(null, answers) : undefined arg.filter = opts.filter ? opts.filter.bind(null, answers) : undefined - - if (opts.choices) { - arg.type = opts.multi ? 'checkbox' : 'list' - arg.choices = opts.choices - } - return arg } diff --git a/lib/conf.js b/lib/conf.js index bd8c958..b6aa042 100644 --- a/lib/conf.js +++ b/lib/conf.js @@ -1,8 +1,6 @@ 'use strict' -const os = require('os') const path = require('path') -const chalk = require('chalk') const pkgup = require('pkg-up') const set = require('mout/object/set') const get = require('mout/object/get') diff --git a/lib/usage/type-of.js b/lib/usage/type-of.js index fd39e6a..4e07df3 100644 --- a/lib/usage/type-of.js +++ b/lib/usage/type-of.js @@ -33,7 +33,10 @@ function typeOf(thing) { } else if (typeof thing === 'number' && isNaN(thing)) { return 'NaN' } else if (Array.isArray(thing)) { - return typeOf(thing[0]).toLowerCase() + '*' + const clean = thing.filter((item) => { + return item !== Array + }) + return typeOf(clean[0]).toLowerCase() + '*' } else if (isFunction(thing)) { return typeOf(thing()).toLowerCase() } else { diff --git a/package.json b/package.json index 03d0d65..fda0153 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ ], "eslintConfig": { "root": true, - "extends": "eslint-config-codedependant", + "extends": "codedependant", "rules": { "no-eq-null": 0, "no-var": 2, @@ -115,7 +115,8 @@ "coverage-report": [ "text", "text-summary", - "json" + "json", + "html" ], "files": [ "test/**/*.js" @@ -123,6 +124,7 @@ "nyc-arg": [ "--exclude=test/", "--exclude=examples/", + "--exclude=release.config.js", "--all" ] } diff --git a/test/command.spec.js b/test/command.js similarity index 91% rename from test/command.spec.js rename to test/command.js index c7cf007..bea4f80 100644 --- a/test/command.spec.js +++ b/test/command.js @@ -126,13 +126,17 @@ test('command', async (t) => { const a = new Command({ name: 'a' , usage: ['get a'] - , run: async function() {} + , run: async function() { + return null + } }) const b = new Command({ name: 'b' , usage: ['get b'] - , run: async function() {} + , run: async function() { + return null + } }) const CmdA = new Command({ @@ -168,7 +172,7 @@ test('command', async (t) => { { const content = await Help.run() t.match(content.trim(), /Invalid command:/) - t.match(content, /\$ command\.spec get c/) + t.match(content, /\$ command get c/) } }) }) @@ -386,6 +390,20 @@ test('command', async (t) => { }) }) + t.test('Command static run function', async (t) => { + async function run() { + return 'static call' + } + class StaticCommand extends Command { + constructor(opts = {}) { + super(opts, {run}) + } + } + + const out = await StaticCommand.run() + t.equal(out, 'static call', 'static run function output') + }) + t.test('Subclassing', async (tt) => { test('should allow for subclassing', async (t) => { const defaults = { @@ -565,6 +583,44 @@ test('command', async (t) => { }) }) + t.test('command#toPrompt', async (t) => { + const cmd = new Command({ + flags: { + one: { + type: Boolean + , description: 'boolean flag' + , when: () => { + return true + } + } + , two: { + type: [Number, Array] + , filter: (val) => { + return val + } + } + } + }) + + const out = cmd.toPrompt() + t.ok(Array.isArray(out), 'output is an array') + t.equal(out.length, 2, 'expected prompt count') + + t.match(out, [{ + type: 'confirm' + , message: 'one: boolean flag' + , when: Function + , validate: undefined + , filter: undefined + }, { + type: 'number' + , message: /no description/ig + , when: undefined + , validate: undefined + , filter: Function + }]) + }) + t.test('sub command execution', async (t) => { const bub = new Command({ name: 'bub' diff --git a/test/flag-to-prompt.js b/test/flag-to-prompt.js new file mode 100644 index 0000000..3382235 --- /dev/null +++ b/test/flag-to-prompt.js @@ -0,0 +1,44 @@ +'use strict' + +const {test, threw} = require('tap') +const flagToPrompt = require('../lib/command/flag-to-prompt.js') + +test('flagToPrompt', async (t) => { + + t.test('no description', async (t) => { + const out = flagToPrompt('test:flag', { + type: Boolean + }) + + t.match(out, { + name: 'test:flag' + , type: 'confirm' + , message: 'test flag: (no description)' + , when: undefined + , validate: undefined + , filter: undefined + }) + }) + + t.test('description w/ input functions', async (t) => { + const out = flagToPrompt('foobar', { + type: String + , description: 'hello world' + , multi: true + , choices: ['one'] + , when: () => {} + , validate: () => {} + , filter: () => {} + }) + + t.match(out, { + name: 'foobar' + , type: 'checkbox' + , message: 'foobar: hello world' + , choices: ['one'] + , when: Function + , validate: Function + , filter: Function + }) + }) +}).catch(threw) diff --git a/test/flag-type.js b/test/flag-type.js new file mode 100644 index 0000000..d2a2f06 --- /dev/null +++ b/test/flag-type.js @@ -0,0 +1,27 @@ +'use strict' + +const path = require('path') +const url = require('url') +const {test, threw} = require('tap') +const flagType = require('../lib/command/flag-type') + +test('flagType', async (t) => { + + const cases = [ + [{type: Boolean}, 'confirm', 'Boolean === confirm'] + , [{type: Number}, 'number', 'Number === number'] + , [{type: String}, 'input', 'String === input'] + , [{type: path}, 'input', 'path === input'] + , [{type: url}, 'input', 'url === input'] + , [{type: [Number, Array]}, 'number', '[Number, Array] === number'] + , [{type: String, mask: true}, 'password', 'mask=true === password'] + , [{type: String, choices: []}, 'list', 'choices === list'] + , [{type: String, choices: [], multi: true}, 'checkbox', 'choices + multi === checkbox'] + , [{type: Function}, 'input', 'unexpected type === input'] + ] + + for (const [flag, expected, msg] of cases) { + const actual = flagType(flag) + t.equal(actual, expected, msg) + } +}).catch(threw) diff --git a/test/lang.spec.js b/test/lang.js similarity index 77% rename from test/lang.spec.js rename to test/lang.js index 570e268..21bb0c6 100644 --- a/test/lang.spec.js +++ b/test/lang.js @@ -7,8 +7,8 @@ const object = require('../lib/lang/object/') const typeOf = require('../lib/usage/type-of') const test = tap.test -test('object', (t) => { - t.test('#set', (tt) => { +test('object', async (t) => { + t.test('#set', async (tt) => { const a = object.set({}, 'a:b:c:d', 1) tt.deepEqual(a, { a: { @@ -30,9 +30,21 @@ test('object', (t) => { } } }) - tt.end() }) - t.end() + + t.test('#merge', async (t) => { + const one = {a: {b: [1]}, c: path, d: url, x: {y: {z: 1}}} + const two = {x: {y: {f: 3}}} + const out = object.merge(one, two) + t.deepEqual(out.c, path, 'path module intact') + t.deepEqual(out.d, url, 'url module intact') + t.deepEqual(out, { + a: {b: [1]} + , c: path + , d: url + , x: {y: {z: 1, f: 3}} + }) + }) }) test('type-of', (t) => { diff --git a/test/registry.spec.js b/test/registry.js similarity index 100% rename from test/registry.spec.js rename to test/registry.js diff --git a/test/seeli.spec.js b/test/seeli.js similarity index 100% rename from test/seeli.spec.js rename to test/seeli.js