Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

makitso

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

makitso - npm Package Compare versions

Comparing version 0.1.2 to 1.0.0

examples/commands/01-hello_world.js

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"

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc