Comparing version 0.1.2 to 1.0.0
121
index.js
@@ -1,120 +0,5 @@ | ||
"use strict"; | ||
const Makitso = require("./lib"); | ||
const Plugins = require("./lib/plugins"); | ||
const _merge = require("lodash/merge"); | ||
const _flatten = require("lodash/flatten"); | ||
const cmd = require("./lib/command"); | ||
const Context = require("./lib/context"); | ||
const DefaultStores = require("./lib/plugins/default-stores"); | ||
const FileStore = require("./lib/plugins/store/file-store"); | ||
const KeychainStore = require("./lib/plugins/store/keychain-store"); | ||
const MemoryStore = require("./lib/plugins/store/memory-store"); | ||
/** | ||
* Register a plugin | ||
* | ||
* @param {Object} args - args | ||
* @param {Object} args.pluginSet - registered plugins | ||
* @param {Object} args.pluginSet.schema - schema | ||
* @param {Object} args.pluginSet.stores - stores | ||
* @param {Object} args.pluginSet.commands - commands | ||
* @param {Object} args.plugin - plugin to register | ||
* @param {Object} args.plugin.schema - schema | ||
* @param {Object} args.plugin.stores - stores | ||
* @param {Object} args.plugin.commands - commands | ||
* @returns {Object} the updated pluginSet | ||
*/ | ||
function registerPlugin(args) { | ||
const { pluginSet, plugin } = args; | ||
if (plugin.schema) { | ||
_merge(pluginSet.schema, plugin.schema); | ||
} | ||
if (plugin.stores) { | ||
_merge(pluginSet.stores, plugin.stores); | ||
} | ||
if (plugin.commands) { | ||
if (plugin.config && plugin.config.command) { | ||
// group commands according to plugin config | ||
_merge(pluginSet.commands, { [plugin.config.command]: plugin.commands }); | ||
} else { | ||
_merge(pluginSet.commands, plugin.commands); | ||
} | ||
} | ||
return pluginSet; | ||
} | ||
/** | ||
* | ||
* @param {Object} args - args | ||
* @param {Object} args.options - | ||
* @param {Object} args.options.app - app options | ||
* @param {String} args.options.app.description - app description | ||
* @param {String} args.options.app.version - app version | ||
* @param {Object} args.options.prompt - prompt options | ||
* @param {String} [args.options.prompt.message="Makitso>"] - text to display as the prompt | ||
* @param {Object} args.options.fileStore - file store options | ||
* @param {String} [args.options.fileStore.file="~/.commandit/file-store.json"] - location of the file store | ||
* @returns {Object} context | ||
*/ | ||
function Makitso(args) { | ||
const { options } = args; | ||
let pluginSet = { schema: {}, stores: {}, commands: {} }; | ||
return { | ||
/** | ||
* Register Plugins | ||
* | ||
* @param {Object|Array} plugins - plugins to register | ||
* @returns {void} | ||
*/ | ||
registerPlugins(...args) { | ||
return Promise.all(_flatten(args)).then(plugins => | ||
plugins.forEach( | ||
plugin => (pluginSet = registerPlugin({ pluginSet, plugin })) | ||
) | ||
); | ||
}, | ||
/** | ||
* Start the app | ||
* @returns {void} | ||
*/ | ||
start() { | ||
const { schema, stores, commands } = pluginSet; | ||
const context = Context({ schema, stores }); | ||
cmd({ context, commands, options }).catch(console.error); | ||
} | ||
}; | ||
} | ||
module.exports = Makitso; | ||
Makitso.DefaultStores = DefaultStores; | ||
Makitso.MemoryStore = MemoryStore; | ||
Makitso.FileStore = FileStore; | ||
Makitso.KeychainStore = KeychainStore; | ||
process.stdin.resume(); // so the program will not close instantly | ||
function exitHandler(options, err) { | ||
if (options.cleanup) { | ||
console.log("clean"); | ||
} | ||
if (err) { | ||
console.log(err.stack); | ||
} | ||
if (options.exit) { | ||
process.exit(); | ||
} | ||
} | ||
// do something when app is closing | ||
process.on("exit", exitHandler.bind(null, { cleanup: true })); | ||
// catches ctrl+c event | ||
process.on("SIGINT", exitHandler.bind(null, { exit: true })); | ||
// catches "kill pid" (for example: nodemon restart) | ||
process.on("SIGUSR1", exitHandler.bind(null, { exit: true })); | ||
process.on("SIGUSR2", exitHandler.bind(null, { exit: true })); | ||
// catches uncaught exceptions | ||
process.on("uncaughtException", exitHandler.bind(null, { exit: true })); | ||
Makitso.Plugins = Plugins; |
@@ -1,78 +0,204 @@ | ||
const parseArgs = require("yargs-parser"); | ||
const parse = require("../args"); | ||
const applyArgNames = require("../args"); | ||
const getArgNames = applyArgNames.getArgNames; | ||
describe("args", () => { | ||
describe("parse", () => { | ||
test("a standard arg with a number value is always formatted as a number", () => { | ||
const appCmd = { args: [{ name: "std" }] }; | ||
const cmdArgs = "123"; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: { std: 123 }, | ||
missing: [], | ||
current: "std" | ||
}); | ||
}); | ||
describe("Args", () => { | ||
describe("getArgNames", () => { | ||
it("returns the names of positional arguments", () => { | ||
const args = "one two three -p four --thing five"; | ||
expect(getArgNames(args)).toEqual(["one", "two", "three"]); | ||
test("a standard arg with a string value is always formatted as a string", () => { | ||
const appCmd = { args: [{ name: "std" }] }; | ||
const cmdArgs = "hello"; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: { std: "hello" }, | ||
missing: [], | ||
current: "std" | ||
}); | ||
}); | ||
}); | ||
describe("applyArgNames", () => { | ||
it("names positional arguments", () => { | ||
const inputArgs = parseArgs("hello world"); | ||
const args = { appCmd: { args: "what who" }, inputArgs }; | ||
expect(applyArgNames(args)).toEqual({ | ||
assignedInputArgs: { who: "world", what: "hello" } | ||
test('when a standard arg with no alias has multiple values supplied, the extra values are "unknown"', () => { | ||
const appCmd = { args: {} }; | ||
const cmdArgs = "hello world"; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: {}, | ||
unknownArgs: ["hello", "world"], | ||
missing: [] | ||
}); | ||
}); | ||
it("identifies missing positional arguments by name", () => { | ||
const inputArgs = parseArgs("hello"); | ||
const args = { appCmd: { args: "what who" }, inputArgs }; | ||
expect(applyArgNames(args)).toEqual({ | ||
assignedInputArgs: { what: "hello" }, | ||
missing: ["who"] | ||
test('undefined options are "unknown"', () => { | ||
const appCmd = { | ||
args: [{ name: "one" }], | ||
optsLookup: {}, | ||
aliasLookup: {} | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "hello --two world -t world" })).toEqual({ | ||
args: { one: "hello" }, | ||
unknownOpts: [{ two: ["world"] }, { t: ["world"] }], | ||
missing: [], | ||
current: "one" | ||
}); | ||
expect(parse({ appCmd, cmdArgs: "hello --two wor --two ld" })).toEqual({ | ||
args: { one: "hello" }, | ||
unknownOpts: [{ two: ["wor", "ld"] }], | ||
missing: [], | ||
current: "one" | ||
}); | ||
expect(parse({ appCmd, cmdArgs: "hello --t wor --t ld" })).toEqual({ | ||
args: { one: "hello" }, | ||
unknownOpts: [{ t: ["wor", "ld"] }], | ||
missing: [], | ||
current: "one" | ||
}); | ||
}); | ||
it("identifies extra positional arguments by value", () => { | ||
const inputArgs = parseArgs("hello Jon age 8"); | ||
const args = { appCmd: { args: "what who" }, inputArgs }; | ||
expect(applyArgNames(args)).toEqual({ | ||
assignedInputArgs: { what: "hello", who: "Jon" }, | ||
unknown: ["age", 8] | ||
test('when a standard arg has no value supplied it is named as "missing"', () => { | ||
const appCmd = { | ||
args: [{ name: "std" }, { name: "std2" }, { name: "std3" }] | ||
}; | ||
const cmdArgs = "hello"; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: { std: "hello" }, | ||
missing: [{ name: "std2" }, { name: "std3" }], | ||
current: "std" | ||
}); | ||
}); | ||
it("includes options", () => { | ||
const inputArgs = parseArgs("-a -bc --what hello --who world"); | ||
const args = { appCmd: {}, inputArgs }; | ||
expect(applyArgNames(args)).toEqual({ | ||
assignedInputArgs: { | ||
a: true, | ||
b: true, | ||
c: true, | ||
what: "hello", | ||
who: "world" | ||
} | ||
test("a multi arg is always formatted as an array", () => { | ||
const appCmd = { args: [{ name: "multi", isMulti: true }] }; | ||
const cmdArgs = "thing"; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: { multi: ["thing"] }, | ||
missing: [], | ||
current: "multi" | ||
}); | ||
}); | ||
it("assigns positional arguments", () => { | ||
const inputArgs = parseArgs("hello 2 -p world --thing 5 -a -bc --true"); | ||
const args = { | ||
appCmd: { | ||
args: "say howmany oops -p productId --debug" | ||
}, | ||
inputArgs | ||
test("an optional arg will not be named as missing", () => { | ||
const appCmd = { args: [{ name: "optional", isOptional: true }] }; | ||
const cmdArgs = ""; | ||
expect(parse({ appCmd, cmdArgs })).toEqual({ | ||
args: {}, | ||
missing: [], | ||
current: "optional" | ||
}); | ||
}); | ||
test("an optional multi arg can be undefined", () => { | ||
const appCmd = { | ||
args: [ | ||
{ name: "std" }, | ||
{ name: "optMulti", isMulti: true, isOptional: true } | ||
] | ||
}; | ||
expect(applyArgNames(args)).toEqual({ | ||
assignedInputArgs: { | ||
a: true, | ||
b: true, | ||
c: true, | ||
howmany: 2, | ||
p: "world", | ||
say: "hello", | ||
thing: 5, | ||
true: true | ||
}, | ||
missing: ["oops"] | ||
expect(parse({ appCmd, cmdArgs: "hello" })).toEqual({ | ||
args: { std: "hello" }, | ||
missing: [], | ||
current: "std" | ||
}); | ||
}); | ||
test("an optional multi arg can have a single value", () => { | ||
const appCmd = { | ||
args: [ | ||
{ name: "std" }, | ||
{ name: "optMulti", isMulti: true, isOptional: true } | ||
] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "hello world" })).toEqual({ | ||
args: { std: "hello", optMulti: ["world"] }, | ||
missing: [], | ||
current: "optMulti" | ||
}); | ||
}); | ||
test("an optional multi arg can have multiple values", () => { | ||
const appCmd = { | ||
args: [ | ||
{ name: "std" }, | ||
{ name: "optMulti", isMulti: true, isOptional: true } | ||
] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "hello there, world" })).toEqual({ | ||
args: { std: "hello", optMulti: ["there,", "world"] }, | ||
missing: [], | ||
current: "optMulti" | ||
}); | ||
}); | ||
test("parser errors are thrown", () => { | ||
const appCmd = { | ||
args: [{ name: "std" }], | ||
argsParserOpts: { coerce: { word: val => val.toLowercase() } } | ||
}; | ||
const cmdArgs = "hello --word blah"; | ||
expect(() => parse({ appCmd, cmdArgs })).toThrow( | ||
"val.toLowercase is not a function" | ||
); | ||
}); | ||
describe("current", () => { | ||
test("current is undefined when there are no defined args", () => { | ||
const appCmd = { | ||
args: [] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "", cmdLine: "do " })).toEqual({ | ||
args: {}, | ||
missing: [] | ||
}); | ||
}); | ||
test("current is first arg when there is no input", () => { | ||
const appCmd = { | ||
args: [{ name: "first" }, { name: "second" }] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "", cmdLine: "do " })).toEqual({ | ||
args: {}, | ||
current: "first", | ||
missing: [{ name: "first" }, { name: "second" }] | ||
}); | ||
}); | ||
test("current is first arg when there is incomplete input", () => { | ||
const appCmd = { | ||
args: [{ name: "first" }, { name: "second" }] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "abc", cmdLine: "do abc" })).toEqual({ | ||
args: { first: "abc" }, | ||
current: "first", | ||
missing: [{ name: "second" }] | ||
}); | ||
}); | ||
test("current is second arg when there is complete input for first", () => { | ||
const appCmd = { | ||
args: [{ name: "first" }, { name: "second" }] | ||
}; | ||
expect(parse({ appCmd, cmdArgs: "abc", cmdLine: "do abc " })).toEqual({ | ||
args: { first: "abc" }, | ||
current: "second", | ||
missing: [{ name: "second" }] | ||
}); | ||
}); | ||
test("current is second arg when there is complete input for first", () => { | ||
const appCmd = { | ||
args: [{ name: "first" }, { name: "second" }] | ||
}; | ||
expect( | ||
parse({ appCmd, cmdArgs: "abc def", cmdLine: "do abc def" }) | ||
).toEqual({ | ||
args: { first: "abc", second: "def" }, | ||
current: "second", | ||
missing: [] | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -0,64 +1,155 @@ | ||
const yargsParseArgs = require("yargs-parser"); | ||
const _forEach = require("lodash/forEach"); | ||
const debug = require("../debug"); | ||
/** | ||
* returns a list of command argument names | ||
* list missing args | ||
* | ||
* @param {String} appCmdArgs - the command args definition | ||
* @returns {String[]} a list of argument names | ||
* @param {Object} appCmd - command definition | ||
* @param {Array} positionalArgs from commandline | ||
* @returns {Array[]} missing args | ||
*/ | ||
function getArgNames(appCmdArgs) { | ||
if (!appCmdArgs) { | ||
return []; | ||
function findMissingArgs(appCmd, positionalArgs) { | ||
let missing = []; | ||
_forEach(appCmd.args, (arg, idx) => { | ||
if (!positionalArgs[idx]) { | ||
if (!arg.isOptional) { | ||
missing.push(arg); | ||
} | ||
} | ||
}); | ||
return missing; | ||
} | ||
/** | ||
* clean and format options specified in the command | ||
* | ||
* @param {Object} appCmd - command definition | ||
* @param {Object} argv - options parsed from commandline | ||
* @returns {Object} argv and unknown options | ||
*/ | ||
function processOptions(appCmd, argv) { | ||
const newArgv = {}; | ||
let unknownOpts = []; | ||
_forEach(argv, (val, key) => { | ||
// don't add aliases | ||
if (!appCmd.aliasLookup[key]) { | ||
// ensure options are arrays unless boolean | ||
if (val !== true && val !== false) { | ||
val = Array.isArray(val) ? val : [val]; | ||
} | ||
// classify unknown options | ||
if (appCmd.optsLookup[key] === undefined) { | ||
unknownOpts.push({ [key]: val }); | ||
} else { | ||
newArgv[key] = val; | ||
} | ||
} | ||
}); | ||
return { unknownOpts, args: newArgv }; | ||
} | ||
/** | ||
* | ||
* @param {Number} argCount - the number of args on commandline | ||
* @param {Boolean} nextArg - true if the last arg on commandline is complete | ||
* @param {Array} argsDef - the command arguments definition | ||
* @returns {String} the current arg | ||
*/ | ||
function getCurrentArg(argCount, nextArg, argsDef) { | ||
let current; | ||
if (argsDef && argsDef.length) { | ||
const lastIdx = argsDef.length - 1; | ||
let idx = argCount; | ||
if (idx && !nextArg) { | ||
idx--; | ||
} | ||
if (idx < argsDef.length) { | ||
current = argsDef[idx].name; | ||
} else if (argsDef[lastIdx].isMulti) { | ||
current = argsDef[lastIdx].name; | ||
} | ||
} | ||
return appCmdArgs.split(/\s+-\w/)[0].split(/\s+/); | ||
return current; | ||
} | ||
/** | ||
* Assign positional args to relative keys | ||
* @typedef {Object} Args | ||
* @property {Array} missing - required arguments not provided | ||
* @property {Array} unknownArgs - arguments which do not exist in the command definition | ||
* @property {Array} unknownOpts - options which do not exist in the command definition | ||
* @property {Object} args - all valid arguments and options from commandline | ||
*/ | ||
/** | ||
* parse command args and options | ||
* | ||
* @param {Object} args - | ||
* @param {Object} args.appCmd - a command definition | ||
* @param {Object} args.inputArgs - args parsed by yargs-parser | ||
* @returns {Object} args, missing, and unknown | ||
* @param {Object} appCmd - command definition | ||
* @param {String} cmdArgs - arguments and options from commandline | ||
* @param {Object} cmdLine - commandLine string | ||
* @returns {Args} the parsed commandline | ||
*/ | ||
function applyArgNames(args) { | ||
const { appCmd, inputArgs } = args; | ||
const defWords = getArgNames(appCmd.args); | ||
const { _: inputPos, ...assignedInputArgs } = inputArgs; | ||
let optional; | ||
let unknown = []; | ||
let missing = []; | ||
const lastArgName = defWords[defWords.length - 1]; | ||
function parseArgs(appCmd, cmdArgs, cmdLine) { | ||
let parsed = yargsParseArgs.detailed(cmdArgs, appCmd.argsParserOpts); | ||
if (/^\[/.test(lastArgName)) { | ||
optional = lastArgName.replace(/[\\[\]]/g, ""); | ||
defWords[defWords.length - 1] = optional; | ||
if (parsed.error) { | ||
throw parsed.error; | ||
} | ||
_forEach(defWords, (word, idx) => { | ||
if (!inputPos[idx]) { | ||
if (word !== optional) { | ||
missing.push(word); | ||
let { _: positionalArgs, ...args } = parsed.argv; | ||
// if positional args have no value they need to appear in missing | ||
let missing = findMissingArgs(appCmd, positionalArgs); | ||
let unknownOpts; | ||
({ args, unknownOpts } = processOptions(appCmd, args)); | ||
let unknownArgs = []; | ||
const lastIdx = appCmd.args ? appCmd.args.length - 1 : -1; | ||
const lastArg = lastIdx >= 0 ? appCmd.args[lastIdx] : {}; | ||
// assign positional values | ||
_forEach(positionalArgs, (value, idx) => { | ||
if (appCmd.args && appCmd.args[idx]) { | ||
if (appCmd.args[idx].isMulti) { | ||
args[appCmd.args[idx].name] = [value]; | ||
} else { | ||
args[appCmd.args[idx].name] = value; | ||
} | ||
return true; | ||
} else if (lastArg.isMulti) { | ||
args[lastArg.name].push(value); | ||
} else { | ||
unknownArgs.push(value); | ||
} | ||
assignedInputArgs[word] = inputPos[idx]; | ||
}); | ||
if (inputPos.length > defWords.length) { | ||
_forEach(inputPos, (word, idx) => { | ||
if (!defWords[idx]) { | ||
unknown.push(word); | ||
} | ||
}); | ||
const argCount = positionalArgs.length; | ||
const nextArg = /\s$/.test(cmdLine); | ||
const current = getCurrentArg(argCount, nextArg, appCmd.args); | ||
const result = { args, missing, current }; | ||
if (unknownArgs.length) { | ||
result.unknownArgs = unknownArgs; | ||
} | ||
const result = { assignedInputArgs }; | ||
if (missing.length) { | ||
result.missing = missing; | ||
if (unknownOpts.length) { | ||
result.unknownOpts = unknownOpts; | ||
} | ||
if (unknown.length) { | ||
result.unknown = unknown; | ||
} | ||
return result; | ||
} | ||
exports = module.exports = applyArgNames; | ||
exports.getArgNames = getArgNames; | ||
/** | ||
* | ||
* @param {Object} arg0 - | ||
* @param {Object} arg0.appCmd - command definition | ||
* @param {String} arg0.cmdArgs - arguments and options input | ||
* @returns {Args} parsed args with validation | ||
*/ | ||
function parse(arg0) { | ||
let { appCmd, cmdArgs, cmdLine } = arg0; | ||
debug({ appCmd, cmdArgs }); | ||
const parsedArgs = parseArgs(appCmd, cmdArgs, cmdLine); | ||
debug({ parsedArgs }); | ||
return parsedArgs; | ||
} | ||
exports = module.exports = parse; |
@@ -1,3 +0,1 @@ | ||
const parseArgs = require("yargs-parser"); | ||
const _forEach = require("lodash/forEach"); | ||
@@ -10,34 +8,38 @@ const _trim = require("lodash/trim"); | ||
* | ||
* @param {Object} args - | ||
* @param {Object} args.cmdLine - | ||
* @param {Object} args.appCommands - app commands definition | ||
* @param {Object} arg0 - | ||
* @param {Object} arg0.cmdLine - | ||
* @param {Object} arg0.commands - app commands definition | ||
* @returns {Object} { appCmd, cmdPath, cmdArgs } | ||
*/ | ||
async function findCommand(args) { | ||
const { cmdLine, appCommands } = args; | ||
async function findCommand(arg0) { | ||
const { cmdLine, commands } = arg0; | ||
const cleanCmdLine = _trim(cmdLine); | ||
if (!cleanCmdLine) { | ||
return {}; | ||
return; | ||
} | ||
const words = cleanCmdLine.split(/\s+/); | ||
let appCmd = appCommands; | ||
let appCmd = { commands }; | ||
let cmdPath = []; | ||
_forEach([...words], word => { | ||
if (appCmd.action) { | ||
if (!appCmd.commands) { | ||
return false; // there are no sub-commands | ||
} | ||
if (!appCmd[word]) { | ||
if (!appCmd.commands[word]) { | ||
return false; // partial command or arg | ||
} | ||
cmdPath.push(words.shift()); | ||
appCmd = appCmd[word]; | ||
appCmd = appCmd.commands[word]; | ||
}); | ||
const inputArgs = parseArgs(words); | ||
if (appCmd === appCommands) { | ||
return { inputArgs }; | ||
if (appCmd.commands === commands) { | ||
return; | ||
} | ||
return { appCmd, cmdPath, inputArgs }; | ||
return { | ||
appCmd, | ||
cmdPath, | ||
cmdArgs: words.join(" "), | ||
cmdArgsList: words | ||
}; | ||
} | ||
exports = module.exports = findCommand; |
"use strict"; | ||
const inquirer = require("inquirer"); | ||
const term = require("terminal-kit").terminal; | ||
const chalk = require("chalk").default; | ||
const history = require("makitso-prompt/key-press-history"); | ||
const assignPositionalArgs = require("./args"); | ||
const autoCompleteFn = require("./autocomplete"); | ||
const debug = require("../debug"); | ||
const parse = require("./args"); | ||
const findCommand = require("./find"); | ||
const iPrompt = require("./prompt"); | ||
const validateFn = require("./validation"); | ||
const Autocomplete = require("./keypress-autocomplete"); | ||
const CommandInfo = require("./keypress-command-info"); | ||
const Validate = require("./keypress-validate"); | ||
inquirer.registerPrompt("custom", iPrompt); | ||
const prompt = inquirer.prompt; | ||
const True = () => true; | ||
/** | ||
* | ||
* @param {Object} context - an instance of Context | ||
* @param {Error} err - an Error | ||
* @returns {void} | ||
*/ | ||
function logError(context, err) { | ||
console.log(chalk.green("Error: "), err.message); | ||
if (context.debug || process.env.MAKITSO_DEBUG) { | ||
console.error(err); | ||
} | ||
} | ||
/** | ||
* Prompt the user for a command | ||
* | ||
* @param {Object} args - | ||
* @param {Function} args.complete - autocomplete | ||
* @param {Function} args.validate - command validation | ||
* @param {Object} args.options - prompt options | ||
* @returns {String} the command from the commandline | ||
*/ | ||
async function commandPrompt(args) { | ||
const { complete, validate, options = {} } = args; | ||
const { message = "CommandIt>" } = options; | ||
const { cmdLine } = await prompt({ | ||
type: "custom", | ||
name: "cmdLine", | ||
message, | ||
autoCompletion: complete, | ||
validate | ||
}); | ||
return cmdLine; | ||
} | ||
/** | ||
* Prompt the user for a command and run it | ||
* | ||
* @param {Object} args - | ||
* @param {Object} args.context - An instance of context | ||
* @param {Object} args.commands - app commands | ||
* @param {Object} args.options - app options | ||
* @param {Function|Array} [args.complete=[]] - prompt autocomplete | ||
* @param {Function} [args.validate] - prompt validation | ||
* @param {Object} arg0 - | ||
* @param {Object} arg0.context - An instance of context | ||
* @param {Object} arg0.commands - app commands | ||
* @param {Object} arg0.options - app options | ||
* @param {Function|Array} [arg0.complete=[]] - prompt autocomplete | ||
* @param {Function} [arg0.validate] - prompt validation | ||
* @returns {void} | ||
*/ | ||
async function promptAndRun(args) { | ||
const { context, appCommands, complete = [], validate = True } = args; | ||
const options = args.options.prompt; | ||
async function promptAndRun(arg0) { | ||
const { context, commands, prompt } = arg0; | ||
try { | ||
term.moveTo(0, term.height - 1); | ||
const cmdLine = await commandPrompt({ complete, validate, options }); | ||
const { inputArgs, appCmd } = await findCommand({ cmdLine, appCommands }); | ||
if (appCmd) { | ||
const { assignedInputArgs } = assignPositionalArgs({ | ||
appCmd, | ||
inputArgs | ||
}); | ||
term.down(1).eraseLine(); | ||
await appCmd.action(context, assignedInputArgs); | ||
term.scrollUp(2); | ||
const cmdLine = await prompt.start(); | ||
debug({ cmdLine }); | ||
const cmd = await findCommand({ cmdLine, commands }); | ||
if (cmd) { | ||
const { appCmd, cmdArgs } = cmd; | ||
const input = parse({ appCmd, cmdArgs }); | ||
debug({ appCmd, cmdArgs, input }); | ||
await appCmd.action({ context, input }); | ||
} | ||
return promptAndRun(arg0); | ||
} catch (error) { | ||
logError(context, error); | ||
if (error.message !== "quit") { | ||
return Promise.reject(error); | ||
} | ||
} | ||
promptAndRun(args); | ||
} | ||
function initCommandPrompt({ context, commands, prompt }) { | ||
const commandInfo = CommandInfo({ context, commands }); | ||
const keyPressParser = { | ||
keyPress: commandInfo | ||
}; | ||
const keyPressAutocomplete = { | ||
keyPress: Autocomplete({ context, commandInfo }) | ||
}; | ||
const keyPressValidate = { | ||
keyPress: Validate() | ||
}; | ||
Object.assign(prompt, { | ||
keyPressers: [ | ||
...prompt.keyPressers, | ||
history, | ||
keyPressParser, | ||
keyPressAutocomplete, | ||
keyPressValidate | ||
] | ||
}); | ||
return prompt; | ||
} | ||
/** | ||
* Start the console app | ||
* | ||
* @param {Object} args - | ||
* @param {Object} args.context - An instance of context | ||
* @param {Object} args.commands - app commands | ||
* @param {Object} args.options - app options | ||
* @param {Object} arg0 - | ||
* @param {Object} arg0.context - An instance of context | ||
* @param {Object} arg0.commands - app commands | ||
* @returns {void} | ||
*/ | ||
async function start(args) { | ||
const { context, commands, options } = args; | ||
const complete = autoCompleteFn({ context, appCommands: commands }); | ||
const validate = validateFn({ appCommands: commands }); | ||
term.scrollUp(2); | ||
promptAndRun({ context, appCommands: commands, options, complete, validate }); | ||
async function start(arg0) { | ||
const { context, commands, prompt } = arg0; | ||
initCommandPrompt({ context, commands, prompt }); | ||
return promptAndRun({ context, commands, prompt }); | ||
} | ||
module.exports = start; |
@@ -1,10 +0,16 @@ | ||
jest.mock("inquirer"); | ||
const Prompt = require("makitso-prompt"); | ||
const inquirer = require("inquirer"); | ||
const Context = require("../index"); | ||
const MemoryStore = require("../../plugins/store/memory-store"); | ||
const MemoryStore = require("../../plugins/stores/memory-store"); | ||
const prompt = Prompt(); | ||
prompt.start = jest.fn(); | ||
function nextInput(username) { | ||
prompt.start.mockReturnValueOnce(Promise.resolve(username)); | ||
} | ||
describe("get", () => { | ||
it("prompts for a missing value, stores, and returns the answer", async () => { | ||
inquirer.prompt.mockReturnValue({ username: "lecstor" }); | ||
nextInput("lecstor"); | ||
const session = MemoryStore(); | ||
@@ -15,6 +21,4 @@ const schema = { | ||
store: "session", | ||
prompt: { | ||
type: "input", | ||
name: "username", | ||
message: `Enter your username...` | ||
ask: { | ||
prompt: `Enter your username.. ` | ||
} | ||
@@ -24,3 +28,3 @@ } | ||
}; | ||
const context = Context({ schema, stores: { session } }); | ||
const context = Context({ schema, stores: { session }, prompt }); | ||
@@ -30,6 +34,8 @@ let result = await context.get("github.username"); | ||
expect(inquirer.prompt).lastCalledWith({ | ||
message: "Enter your username...", | ||
name: "username", | ||
type: "input" | ||
expect(prompt.start).lastCalledWith({ | ||
default: undefined, | ||
footer: "", | ||
header: "", | ||
prompt: "Enter your username.. ", | ||
maskInput: false | ||
}); | ||
@@ -42,3 +48,3 @@ | ||
it("prompts with a default value, stores, and returns the answer", async () => { | ||
inquirer.prompt.mockReturnValue({ username: "lecstor" }); | ||
nextInput("lecstor"); | ||
const session = MemoryStore(); | ||
@@ -49,13 +55,10 @@ const schema = { | ||
store: "session", | ||
prompt: { | ||
type: "input", | ||
name: "username", | ||
message: `Enter your username...` | ||
}, | ||
promptWithDefault: true, | ||
default: "lecstor" | ||
ask: { | ||
prompt: `Enter your username.. `, | ||
default: "lecstor" | ||
} | ||
} | ||
} | ||
}; | ||
const context = Context({ schema, stores: { session } }); | ||
const context = Context({ schema, stores: { session }, prompt }); | ||
@@ -66,7 +69,8 @@ let result = await context.get("github.username"); | ||
// prompt was called with default | ||
expect(inquirer.prompt).lastCalledWith({ | ||
expect(prompt.start).lastCalledWith({ | ||
default: "lecstor", | ||
message: "Enter your username...", | ||
name: "username", | ||
type: "input" | ||
footer: "", | ||
header: "", | ||
prompt: "Enter your username.. ", | ||
maskInput: false | ||
}); | ||
@@ -80,7 +84,5 @@ | ||
it("prompts with the value from the store as default, stores, and returns the answer", async () => { | ||
inquirer.prompt.mockReturnValue({ username: "lecstor" }); | ||
nextInput("lecstor"); | ||
const session = MemoryStore({ | ||
data: { | ||
github: { username: { default: "lastUsed" } } | ||
} | ||
data: { github: { username: { default: "lastUsed" } } } | ||
}); | ||
@@ -91,13 +93,11 @@ const schema = { | ||
store: "session", | ||
prompt: { | ||
type: "input", | ||
name: "username", | ||
message: `Enter your username...` | ||
}, | ||
promptWithDefault: true, | ||
default: "lecstor" | ||
ask: { | ||
prompt: `Enter your username.. `, | ||
default: "lecstor", | ||
storedValueIs: "default" | ||
} | ||
} | ||
} | ||
}; | ||
const context = Context({ schema, stores: { session } }); | ||
const context = Context({ schema, stores: { session }, prompt }); | ||
@@ -108,7 +108,8 @@ let result = await context.get("github.username"); | ||
// prompt was called with the value in the store | ||
expect(inquirer.prompt).lastCalledWith({ | ||
expect(prompt.start).lastCalledWith({ | ||
default: "lastUsed", | ||
message: "Enter your username...", | ||
name: "username", | ||
type: "input" | ||
footer: "", | ||
header: "", | ||
prompt: "Enter your username.. ", | ||
maskInput: false | ||
}); | ||
@@ -123,10 +124,19 @@ | ||
const session = MemoryStore({ | ||
data: { | ||
github: { username: { default: "lecstor" } } | ||
data: { github: { username: { default: "lecstor" } } } | ||
}); | ||
const schema = { | ||
github: { | ||
username: { | ||
store: "session", | ||
ask: { | ||
prompt: `Enter your username.. `, | ||
default: "lecstor", | ||
storedValueIs: "response" | ||
} | ||
} | ||
} | ||
}); | ||
const schema = { github: { username: { store: "session" } } }; | ||
const context = Context({ schema, stores: { session } }); | ||
}; | ||
const context = Context({ schema, stores: { session }, prompt }); | ||
return expect(context.get("github.username")).resolves.toEqual("lecstor"); | ||
}); | ||
}); |
const Context = require("../index"); | ||
const MemoryStore = require("../../plugins/store/memory-store"); | ||
const MemoryStore = require("../../plugins/stores/memory-store"); | ||
@@ -4,0 +4,0 @@ describe("set", () => { |
@@ -5,3 +5,2 @@ "use strict"; | ||
const _get = require("lodash/get"); | ||
const { prompt } = require("inquirer"); | ||
@@ -63,3 +62,3 @@ /** | ||
* @property {String} store - a store identifier | ||
* @property {Object} prompt - an inquirer question definition | ||
* @property {Object} prompt - a question definition | ||
* @property {String} prompt.type - question type | ||
@@ -91,10 +90,17 @@ * @property {String} prompt.name - question name | ||
* | ||
* @param {Object} args - args | ||
* @param {Object} args.schema - The context schema. | ||
* @param {Object} args.stores - An object of store objects. | ||
* @param {Object} arg0 - | ||
* @param {Object} arg0.commands - Commands definition. | ||
* @param {Object} arg0.schema - Context schema. | ||
* @param {Object} arg0.stores - Object of store objects. | ||
* @param {Object} arg0.prompt - Instance of makitso-prompt | ||
* @returns {Object} An instance of Context. | ||
*/ | ||
function Context(args) { | ||
const { schema, stores } = args; | ||
function Context(arg0 = {}) { | ||
const { commands, prompt, schema, stores } = arg0; | ||
return { | ||
commands, | ||
prompt, | ||
schema, | ||
stores, | ||
/** | ||
@@ -107,21 +113,25 @@ * Get a context property | ||
get: async function(prop) { | ||
const meta = getPropMeta(schema, prop); | ||
let value = await stores[meta.store].get(meta); | ||
if (value && !meta.promptWithDefault) { | ||
const meta = getPropMeta(this.schema, prop); | ||
if (!meta.ask) { | ||
throw new Error(`There is no "ask" set for schema property "${prop}"`); | ||
} | ||
const variant = (meta.variant !== "default" ? meta.variant : false) || ""; | ||
const { header, prompt: mPrompt, footer } = meta.ask; | ||
const { storedValueIs, default: mDefault, maskInput = false } = meta.ask; | ||
let value = await this.stores[meta.store].get(meta); | ||
// use stored value without prompting | ||
if (value && storedValueIs === "response") { | ||
return value; | ||
} | ||
const variant = meta.variant !== "default" ? meta.variant : false; | ||
const message = variant | ||
? meta.prompt.message.replace("{variant}", variant) | ||
: meta.prompt.message.replace("{variant}\\s+", ""); | ||
const thisPrompt = { ...meta.prompt, message }; | ||
if (meta.promptWithDefault) { | ||
value = await prompt({ | ||
...thisPrompt, | ||
default: value || meta.default | ||
}); | ||
} else { | ||
value = await prompt(thisPrompt); | ||
} | ||
const answer = _get(value, meta.prompt.name); | ||
const thisPrompt = { | ||
header: header ? header.replace("{variant}", variant) : "", | ||
prompt: mPrompt ? mPrompt.replace("{variant}", variant) : "", | ||
footer: footer ? footer.replace("{variant}", variant) : "", | ||
default: value && storedValueIs === "default" ? value : mDefault, | ||
maskInput | ||
}; | ||
const answer = await prompt.start(thisPrompt); | ||
await this.set(prop, answer); | ||
@@ -139,7 +149,7 @@ return answer; | ||
set: async function(prop, value) { | ||
const meta = getPropMeta(schema, prop); | ||
if (!stores[meta.store]) { | ||
const meta = getPropMeta(this.schema, prop); | ||
if (!this.stores[meta.store]) { | ||
throw StoreNotFoundError(meta.store); | ||
} | ||
return stores[meta.store].set(meta, value); | ||
return this.stores[meta.store].set(meta, value); | ||
}, | ||
@@ -155,4 +165,4 @@ | ||
delete: async function(prop) { | ||
const meta = getPropMeta(schema, prop); | ||
return stores[meta.store].delete(meta); | ||
const meta = getPropMeta(this.schema, prop); | ||
return this.stores[meta.store].delete(meta); | ||
}, | ||
@@ -166,4 +176,4 @@ | ||
*/ | ||
getStore: async store => { | ||
return stores[store]; | ||
getStore: async function(store) { | ||
return this.stores[store]; | ||
}, | ||
@@ -176,3 +186,5 @@ | ||
*/ | ||
listStores: () => Object.keys(stores), | ||
listStores: async function() { | ||
return Object.keys(this.stores); | ||
}, | ||
@@ -183,4 +195,4 @@ /** | ||
*/ | ||
getSchema: () => { | ||
return JSON.parse(JSON.stringify(schema)); | ||
getSchema: function() { | ||
return JSON.parse(JSON.stringify(this.schema)); | ||
} | ||
@@ -187,0 +199,0 @@ }; |
{ | ||
"name": "makitso", | ||
"version": "0.1.2", | ||
"version": "1.0.0", | ||
"description": "A Framework for building composable interactive commandline apps", | ||
@@ -19,10 +19,10 @@ "main": "index.js", | ||
"ellipsize": "^0.0.3", | ||
"inquirer": "^4.0.0", | ||
"js-yaml": "^3.10.0", | ||
"keytar": "^4.0.5", | ||
"makitso-develop-plugin": "^2.0.0", | ||
"makitso-tunnelblick2fa-plugin": "^2.0.0", | ||
"terminal-kit": "^1.14.3", | ||
"makitso-prompt": "^1.0.1", | ||
"prettyjson": "^1.2.1", | ||
"yargs-parser": "^8.0.0" | ||
}, | ||
"devDependencies": { | ||
"debug": "^3.1.0", | ||
"eslint": "^4.9.0", | ||
@@ -39,5 +39,2 @@ "eslint-config-prettier": "^2.6.0", | ||
}, | ||
"bin": { | ||
"makitso": "./bin/index.js" | ||
}, | ||
"engines": { | ||
@@ -44,0 +41,0 @@ "node": ">=8.9" |
154
README.md
@@ -1,13 +0,27 @@ | ||
Makitso | ||
------- | ||
## Makitso | ||
A Framework for building composable interactive commandline apps. | ||
With Makitso you can build interactive commandline apps with simple plugin | ||
modules which define commands, a property schema, and/or storage | ||
connectors. | ||
The goal is to have a framework that can be used to build a single customised | ||
commandline app that developers in an organisation can easily install and use | ||
to do common tasks, hopefully without much instruction. | ||
To that end, Makitso allows a nested command structure, suggestions and | ||
auto-complete for commands and arguments, and prompting and storage of | ||
configuration properties specific to the user (or app, workspace, etc). | ||
Makitso plugin modules can be used to define commands, context (config) schema, | ||
and storage options. Once defined, context values can be used by commands from | ||
other plugins. If a value is not set the user is prompted to provide it. Once | ||
set, the value can be used as a default answer to a prompt, or returned to the | ||
command directly, depending on it's configuration. | ||
### Context Schema | ||
The schema defines context properties, the storage to use, and a prompt for | ||
collecting the value of the property. | ||
collecting the value of the property. Property values are stored in the memory, | ||
file, or keychain stores for immediate and later use. When an unset value is | ||
requested by a command action the user is prompted for the required value and it | ||
is stored in context and returned to the action. | ||
```js | ||
@@ -19,6 +33,6 @@ { | ||
store: "file", | ||
prompt: { | ||
type: "input", | ||
name: "name", | ||
message: `Enter your {variant} name ...` | ||
ask: { | ||
header: `We'll only ask once..`, | ||
prompt: `{variant} name> `, | ||
footer: `Run this command again when you're done.` | ||
} | ||
@@ -33,76 +47,62 @@ } | ||
Commands define the command arguments format, help description, an action | ||
function, and optional choices function for autocomplete. | ||
Commands define the arguments and options format, help description, an action | ||
function, and optional suggest function for autocomplete. | ||
Commands can be grouped using the config.command property for all | ||
commands in the plugin, and with object nesting within the plugin. Autocomplete | ||
will be supported for each of the command levels. | ||
Commands can be nested to allow grouping commands as sub-commands. | ||
Suggestions and auto-complete are supported for each of the command levels. | ||
Commands can also provide a "choices" property which will allow autocomplete | ||
to provide available options for the command. | ||
Commands can also provide a "suggest" function which will allow autocomplete to | ||
provide available options for the command. | ||
Command action functions receive a context instance which gives access to | ||
properties handled by other plugins. If a required property has not already | ||
been set then the plugin which is handling it will prompt the user to enter it | ||
and then return the entered value. | ||
Command action functions receive context & input objects which gives access to | ||
properties handled by other plugins and the current command on the commandline. | ||
If a required property has not already been set then the plugin which is handling | ||
it will prompt the user to enter it and then return the entered value. | ||
`arguments` define the positional command arguments. The last argument may be | ||
`[optional]`, a `list..`, or `[both...]`. | ||
`description` is displayed as part of the `help` command and as inline helpers | ||
in the input prompts. | ||
`action` is the code to run when the command is used. It receives an object as | ||
it's only arg, containing the context instance and an input object containing | ||
the command arguments and options entered. It can get and set context properties | ||
defined in the context schema and run custom code to get stuff done. | ||
`suggest` recieves the same argiments as `action` and returns a list of commands | ||
or arguments which are displayed to the user. This list is later filtered using | ||
the partial command or argument entered by the user. Auto-complete can be triggered | ||
by the user with the tab key. | ||
```js | ||
{ | ||
commands: { | ||
set: { | ||
args: "prop value", | ||
description: "Set a context value", | ||
action: (context, { prop, value }) => context.set(prop, value) | ||
const Makitso = require("makitso"); | ||
const commands = { | ||
up: { | ||
description: "Bring up one or more services with docker compose", | ||
arguments: ["services... - the service/s to bring up"], | ||
action: async ({ context, input }) => { | ||
const services = input.args.services.join(" "); | ||
console.log(`shelljs.exec docker-compose up -d ${services}`); | ||
}, | ||
printName: { | ||
description: "Print your name", | ||
action: async (context) => { | ||
const firstName = await context.get("my.name.first"); | ||
const lastName = await context.get("my.name.last"); | ||
console.log(`${firstName} ${lastName}`); | ||
} | ||
}, | ||
debug: { | ||
on: { | ||
description: "Turn on debugging", | ||
action: async context => console.log("Debug On") | ||
}, | ||
off: { | ||
description: "Turn off debugging", | ||
action: async context => console.log("Debug Off"); | ||
} | ||
}, | ||
dump: { | ||
store: { | ||
args: "storeId", | ||
description: "Dump the store", | ||
choices: { | ||
storeId: ({ context }) => context.listStores() | ||
}, | ||
action: async (context, { storeId }) => { | ||
const store = await context.getStore(storeId); | ||
console.log(JSON.stringify(await store.read(), null, 2)); | ||
} | ||
} | ||
}, | ||
suggest: async ({ context, input }) => { | ||
// read and parse docker-compose.yml and return the services keys | ||
return ["foo", "bar", "baz"]; | ||
} | ||
}, | ||
config: { | ||
command: "demo" | ||
down: { | ||
description: "Bring down the stack", | ||
action: async ({ context, input }) => { | ||
console.log(`shelljs.exec docker-compose down`); | ||
} | ||
} | ||
} | ||
}; | ||
const myMakitsoPlugin = { commands }; | ||
Makitso({ plugins: myMakitsoPlugin }).catch(console.error); | ||
``` | ||
Usage | ||
``` | ||
$ makitso | ||
? Makitso> demo printName | ||
? Enter your first name ... Jason | ||
? Enter your last name ... Galea | ||
Jason Galea | ||
? Makitso> demo printName | ||
Jason Galea | ||
? Makitso> | ||
``` | ||
See the [builtin app](./bin/index.js) for an example or install this module | ||
globally and try it for yourself. | ||
See [examples](./examples) for examples of different Makitso features. | ||
@@ -113,7 +113,1 @@ ``` | ||
``` | ||
Makitso uses [inquirer](https://github.com/SBoudrias/Inquirer.js), | ||
and a modified version of [inquirer-command-prompt](https://github.com/sullof/inquirer-command-prompt), | ||
and some custom plumbing to hook them up to a central context module backed by | ||
session (memory), file, and secure (keychain) context storage. | ||
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
173305
8
39
2100
1
11
111
4
1
+ Addedjs-yaml@^3.10.0
+ Addedmakitso-prompt@^1.0.1
+ Addedprettyjson@^1.2.1
+ Addedargparse@1.0.10(transitive)
+ Addedcolors@1.4.0(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedjs-yaml@3.14.1(transitive)
+ Addedmakitso-prompt@1.0.3(transitive)
+ Addedprettyjson@1.2.5(transitive)
+ Addedsprintf-js@1.0.3(transitive)
- Removedinquirer@^4.0.0
- Removedmakitso-develop-plugin@^2.0.0
- Removedmakitso-tunnelblick2fa-plugin@^2.0.0
- Removedterminal-kit@^1.14.3
- Removed@cronvel/get-pixels@3.4.1(transitive)
- Removedansi-escapes@3.2.0(transitive)
- Removedansi-regex@3.0.1(transitive)
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedchardet@0.4.2(transitive)
- Removedchroma-js@2.6.0(transitive)
- Removedcli-cursor@2.1.0(transitive)
- Removedcli-width@2.2.1(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedcwise-compiler@1.1.3(transitive)
- Removedexternal-editor@2.2.0(transitive)
- Removedfigures@2.0.0(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedglob@7.2.3(transitive)
- Removedhasown@2.0.2(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinquirer@4.0.2(transitive)
- Removedinterpret@1.4.0(transitive)
- Removediota-array@1.0.0(transitive)
- Removedis-buffer@1.1.6(transitive)
- Removedis-core-module@2.15.1(transitive)
- Removedis-fullwidth-code-point@2.0.0(transitive)
- Removedjpeg-js@0.4.4(transitive)
- Removedlazyness@1.2.0(transitive)
- Removedmakitso-develop-plugin@2.0.1(transitive)
- Removedmakitso-tunnelblick2fa-plugin@2.0.1(transitive)
- Removedmimic-fn@1.2.0(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedmute-stream@0.0.7(transitive)
- Removedndarray@1.0.19(transitive)
- Removedndarray-pack@1.2.1(transitive)
- Removednextgen-events@1.5.3(transitive)
- Removednode-bitmap@0.0.1(transitive)
- Removedomggif@1.0.10(transitive)
- Removedonetime@2.0.1(transitive)
- Removedos-tmpdir@1.0.2(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedpath-parse@1.0.7(transitive)
- Removedpngjs@6.0.0(transitive)
- Removedrechoir@0.6.2(transitive)
- Removedresolve@1.22.8(transitive)
- Removedrestore-cursor@2.0.0(transitive)
- Removedrun-async@2.4.1(transitive)
- Removedrx-lite@4.0.8(transitive)
- Removedrx-lite-aggregates@4.0.8(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsetimmediate@1.0.5(transitive)
- Removedseventh@0.7.40(transitive)
- Removedshelljs@0.7.8(transitive)
- Removedstring-kit@0.11.10(transitive)
- Removedstring-width@2.1.1(transitive)
- Removedstrip-ansi@4.0.0(transitive)
- Removedsupports-preserve-symlinks-flag@1.0.0(transitive)
- Removedterminal-kit@1.49.4(transitive)
- Removedthrough@2.3.8(transitive)
- Removedtmp@0.0.33(transitive)
- Removedtree-kit@0.7.5(transitive)
- Removeduniq@1.0.1(transitive)