New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

completion

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

completion - npm Package Compare versions

Comparing version
0.4.0
to
1.0.0
+52
docs/examples-git.js
var Completion = require('../');
var gitCompletion = new Completion({
name: 'git',
options: [{
// `git --help`, a terminal option
name: '--help'
}],
commands: [{
// `git checkout master`
name: 'checkout',
option: [{
// `git checkout -b dev/hello`
name: '-b',
completion: function (info, cb) {
// `-b` was matched by `completion` so keep on recursing
return this.resolveInfo(info, cb);
}
}],
completion: function getGitBranches (info, cb) {
// Get git branches and find matches
}
}, {
name: 'remote',
commands: [{
// `git remote add origin git@github.com:...`
// No possible completion here
name: 'add'
}, {
// `git remote rm origin`
name: 'rm',
completion: function getGitBranches (info, cb) {
// Get git branches and find matches
}
}]
}]
});
gitCompletion.complete({
// `git remo|add`
line: 'git remoadd',
cursor: 8
}, function (err, results) {
console.log(results); // ['remote']
});
gitCompletion.complete({
// `git remote |`
line: 'git remote ',
cursor: 11
}, function (err, results) {
console.log(results); // ['add', 'remove']
});
var Completion = require('../');
var completion = new Completion({
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
// For `git checkout dev/|`
// info.words.value = ['git', 'checkout', 'dev/']
// info.word.partialLeft = 'dev/'
var that = this;
getGitBranches(function (err, allBranches) {
if (err) {
return cb(err);
}
// Match 'dev/' === 'dev/' (from 'dev/hello')
var partialLeftWord = info.word.partialLeft;
var branches = that.matchLeftWord(partialLeftWord, allBranches);
cb(null, branches);
});
}
}]
});
completion.complete({
// `git chec|`
line: 'git chec',
cursor: 8
}, function (err, results) {
console.log(results); // ['checkout']
});
// Load in dependencies
var assert = require('assert');
var completionUtils = require('./utils/completion');
// Start our tests
describe('A command with terminal options', function () {
completionUtils.init({
name: 'git',
options: [{
// Do not complete anything new after `help`
name: '--help'
}],
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}]
});
describe('completing a terminal option followed by a command', function () {
completionUtils.completeCommand('git --help chec|');
it('does not return any matching commands', function () {
assert.deepEqual(this.results, []);
});
});
describe('completing an option', function () {
completionUtils.completeCommand('git --he|');
it('does not return any values', function () {
assert.deepEqual(this.results, []);
});
});
});
describe('A command with non-terminal options', function () {
completionUtils.init({
name: 'git',
commands: [{
name: 'checkout',
options: [{
name: '-b',
completion: function (info, cb) {
// The `-b` has already been shifted because we matched `-b`
// As a result, attempt to complete once again from `git's` context
this.resolveInfo(info, cb);
}
}],
completion: function (info, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}]
});
describe('completing a command\'s values', function () {
completionUtils.completeCommand('git checkout -b hello|');
it('returns matching values', function () {
assert.deepEqual(this.results, ['hello-world', 'hello-there']);
});
});
});
describe('A command with non-terminal command options', function () {
completionUtils.init({
name: 'git',
options: [{
name: '--dry-run',
completion: function (info, cb) {
// --dry-run has already been shifted, continue resolving
this.resolveInfo(info, cb);
}
}],
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}]
});
describe('completing a command', function () {
completionUtils.completeCommand('git --dry-run chec|');
it('returns a matching command', function () {
assert.deepEqual(this.results, ['checkout']);
});
});
});
var Completion = require('../../');
var cursorUtils = require('./cursor');
// Define set of utilities for `completion`
exports.completeCommand = function (command) {
before(function (done) {
var params = cursorUtils.splitAtCursor(command);
var that = this;
this.completion.complete(params, function (err, results) {
that.results = results;
done(err);
});
});
after(function () {
delete this.results;
});
};
exports.init = function (params) {
before(function initCompletion () {
this.completion = new Completion(params);
});
after(function cleanupCompletion () {
delete this.completion;
});
};
+2
-0
# completion changelog
1.0.0 - Reworked data structure to support options
0.4.0 - Moved onto recursive solver, allowing for completion commands to re-enter context

@@ -3,0 +5,0 @@

+95
-57

@@ -5,14 +5,10 @@ // Load in dependencies

// TODO: In order to make `options` not appear in `object`-based resolutons (e.g. ['-p', 'checkout'])
// allow them to be tagged with a special flag (e.g. `Completion.optional(fn)` -> sets _completionOptional = true)
// Define our completion constructor
function Completion(tree, parentNode) {
function Completion(tree) {
// Save the tree as our node
this.node = tree;
this.parentNode = parentNode;
}
Completion.prototype = {
// Helper functions for working with `info`
matchLeftWord: function (_info) {
shiftLeftWord: function (_info) {
// Prevent mutation on the original `info`

@@ -29,10 +25,9 @@ var info = deepClone(_info);

},
matchLeftWord: function (leftWord, words) {
return words.filter(function findMatchingWords (word) {
return leftWord === word.substr(0, leftWord.length);
});
},
// Define completion methods
complete: function (params, cb) {
// Collect info
var info = lineInfo(params);
return this.completeInfo(info, cb);
},
completeInfo: function (info, cb) {
_guaranteeMatchedInfo: function (info) {
// If there is no `remainingLeft` or `matchedLeft` words, add them

@@ -50,39 +45,61 @@ if (!info.words.remainingLeft || !info.words.matchedLeft) {

return info;
},
// Define completion methods
complete: function (params, cb) {
// Collect info
var info = lineInfo(params);
return this.completeInfo(info, cb);
},
completeInfo: function (info, cb) {
// Remove the newest matching word
info = this._guaranteeMatchedInfo(info);
info = this.shiftLeftWord(info);
var matchedWord = info.words.matchedLeft[info.words.matchedLeft.length - 1];
// If the matched word did not match the command, exit with no results
// `npm pub|` matching ['git', 'checkout'] -> npm !== git
var node = this.node;
if (matchedWord !== node.name) {
return cb(null, []);
}
return this.resolveInfo(info, cb);
},
resolveInfo: function (info, cb) {
// If there are no words left, exit early with nothing
// `npm|` -> ['npm'] matched -> []
info = this._guaranteeMatchedInfo(info);
var node = this.node;
if (info.words.remainingLeft.length === 0) {
return cb(null, []);
}
// The following is a recursive loop that creates child completion's until we arrive
// at the second to last word in the left half of the command
// at the last word in the left half of the command
// `git che|c` -> ['git', 'che'] + ['c'] -> git.che (404) -> git.*.filter('che')
// `git checkout |world` -> ['git', 'checkout'] + ['world'] -> git.checkout(params, cb) (`['world']`)
var node = this.node;
// If we have less than 2 remaining left words, return early
if (info.words.remainingLeft.length < 2) {
// DEV: `node` can be an object (more commands remaining),
// DEV: a function (custom complete logic), or something falsy (e.g. no future autocompletes possible)
// If the second to last node is falsy, callback with nothing
// ['npm', 'publish', ''] on {npm: {publish: null}} -> [] (nothing to complete)
var secondToLastNode = node;
if (!secondToLastNode) {
// If there is 1 word remaining, determine what to do
if (info.words.remainingLeft.length === 1) {
// If there is completion logic, use it
// ['git', 'checkout', 'hello'] on {name: git, commands: [{name: checkout, completion: getBranches}]}
// -> ['hello.word' (branch)]
if (node.completion) {
return node.completion.call(this, info, cb);
// If there are more commands, match them
// ['git', 'che'] on {name: git, commands: [{name: checkout, completion: getBranches}]}
// -> ['checkout']
} else if (node.commands) {
var cmds = node.commands.map(function getCommandName (commandNode) {
return commandNode.name;
});
var partialLeftWord = info.word.partialLeft;
var matchingCmds = this.matchLeftWord(partialLeftWord, cmds);
matchingCmds.sort();
return cb(null, matchingCmds);
// Otherwise, this is a terminal command so callback with nothing
} else {
return cb(null, []);
// Otherwise, attempt to understand what to do
} else {
var partialLeftWord = info.word.partialLeft;
// If the node is a function, find all possible completions
// ['git', 'checkout', 'hello'] on {git: {checkout: getGitBranches}} -> ['hello.word' (branch)]
if (typeof secondToLastNode === 'function') {
return secondToLastNode.call(this, info, cb);
// If the node is an objet, find matching commands
// ['git', 'che'] on {git: {checkout: getGitBranches}} -> ['checkout']
} else if (typeof secondToLastNode === 'object') {
var cmds = Object.getOwnPropertyNames(secondToLastNode);
var matchingCmds = cmds.filter(function (cmd) {
return partialLeftWord === cmd.substr(0, partialLeftWord.length);
});
matchingCmds.sort();
return cb(null, matchingCmds);
// Otherwise, we don't know what to do (not an object or fn)
// so callback with nothing
} else {
return cb(null, []);
}
}

@@ -92,18 +109,39 @@ // Otherwise, attempt to keep on recursing

// Match the newest left word
info = this.matchLeftWord(info);
var nextWord = info.words.remainingLeft[0];
// Find the next node
var matchedWord = info.words.matchedLeft[info.words.matchedLeft.length - 1];
var childNode = node[matchedWord];
// If the next word is an option
var optionNodes = node.options || [];
var matchedOptionNode = optionNodes.filter(function matchoption (optionNode) {
return optionNode.name === nextWord;
})[0];
if (matchedOptionNode) {
// If there is a completion action, match it and use it
if (matchedOptionNode.completion) {
info = this.shiftLeftWord(info);
return matchedOptionNode.completion.call(this, info, cb);
// Otherwise, exit with no results
} else {
return cb(null, []);
}
}
// If there is no new node, exit with no more results
// DEV: This could be `null` as defined by someone's completion case
// DEV: or it could be `undefined` if the completion algorithm has not been defined for this command
if (!childNode) {
cb(null, []);
// Otherwise, if the next word is a command
var commandNodes = node.commands || [];
var matchedCommandNode;
var i = 0;
var len = commandNodes.length;
for (; i < len; i++) {
var commandNode = commandNodes[i];
if (commandNode.name === nextWord) {
matchedCommandNode = commandNode;
}
}
if (matchedCommandNode) {
// Recurse further
var childCompletion = new Completion(matchedCommandNode);
return childCompletion.completeInfo(info, cb);
}
// Otherwise, recurse further
var childCompletion = new Completion(childNode, this);
childCompletion.completeInfo(info, cb);
// Otherwise, there are no more matches and exit with no results
cb(null, []);
}

@@ -110,0 +148,0 @@ }

{
"name": "completion",
"description": "Completion library for words, commands, and sentences",
"version": "0.4.0",
"description": "Completion library for CLI commands",
"version": "1.0.0",
"homepage": "https://github.com/twolfson/completion",

@@ -46,4 +46,7 @@ "author": {

"auto",
"tab"
"tab",
"cli",
"bash",
"zsh"
]
}
+108
-31
# completion [![Build status](https://travis-ci.org/twolfson/completion.png?branch=master)](https://travis-ci.org/twolfson/completion)
Completion library for words, commands, and sentences
Completion library for CLI commands

@@ -23,10 +23,13 @@ This was built as part of [foundry][], a CLI utility for making releases painless.

```javascript
```js
var Completion = require('completion');
var completion = new Completion({
git: {
checkout: function (info, cb) {
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
// For `git checkout dev/|`
// info.words.value = ['git', 'checkout', 'dev/']
// info.word.partialLeft = 'dev/'
var that = this;
getGitBranches(function (err, allBranches) {

@@ -37,7 +40,5 @@ if (err) {

// Match 'dev/' === 'dev/' (from 'dev/hello')
var partialLeftWord = info.word.partialLeft;
var branches = allBranches.filter(function (branch) {
// 'chec' === 'chec' (from 'checkout')
return partialLeftWord === branch.substr(0, partialLeftWord.length);
});
var branches = that.matchLeftWord(partialLeftWord, allBranches);
cb(null, branches);

@@ -85,20 +86,69 @@ });

- tree `Object` - Outline of program
- Each key represents a new command (e.g. `git`, `checkout`)
- Each value can be
- An object representing another layer of commands
- A function that will callback with potential matches
- The function should be error-first; have a signature of `function (info, cb)`
- info `Object` - Collection of distilled information
- The format will be the returned value from [twolfson/line-info][]
- cb `Function` - Error-first callback function to run with matches
- `cb` has a signature of `function (err, results)`
- `null` representing a terminal function which has no further predictive input
- **If you want to list out files, do so. Don't use `null` for that case.**
- tree `Object` - Outline of a program/command
- name `String` - Command that is being executed (e.g. `git`, `checkout`)
- options `Object[]` - Optional array of objects that represent options
- name `String` - Name of option (e.g. `--help`)
- completion `Function` - Optional function to complete the remainder of the invocation
- If no `completion` is specified, we assume this is terminal and stop recursing
- Details on completion functions can be found below
- commands `Object[]` - Optional array of new `tree` instances to complete against
- This cannot exist on the same node as `completion` as they are contradictory
- completion `Function` - Optional completion function to determine results for a command
- Details on completion can be found below
#### `command/option completion` functions
`options` and `commands` share a common completion function signature, `function (info, cb)`
Each `completion` function will be executed with the command node as its `this` context
- info `Object` - Information about original input
- Content will be information from [twolfson/line-info][]
- We provide 2 additional properties
- words.matchedLeft `String[]` - Words matched from `words.partialLeft` while walking the tree
- words.remainingLeft `String[]` - Unmatched words that need to be/can be matched against
- cb `Function` - Error-first callback function to return matches via
- `cb` has a signature of `function (err, results)`
[twolfson/line-info]: https://github.com/twolfson/line-info#lineinfoparams
### `completion.complete(params, cb)`
Get potential completion matches
For options, it is often preferred to remove more words that are matched (e.g. `-m <msg>`). For this, we suggest using the [`shiftLeftWord` method][shift-left-word].
For completing partial matches, we provide the [`matchLeftWord` method][match-left-word].
To create non-terminal options, we can use the [method `resolveInfo`][resolve-info] to keep on searching against the `remainingLeft` words.
[shift-left-word]: #completionshiftleftwordinfo
[match-left-word]: #completionmatchleftwordleftword-words
[resolve-info]: #completionresolveinfoinfo-cb
#### `completion.shiftLeftWord(info)`
Helper function to shift word from `info.words.remainingLeft` to `info.words.matchedLeft`
- info `Object` - Information passed into `completion` functon
```js
var info = {words: {remainingLeft: ['hello', 'world'], matchedLeft: []}};
info = this.shiftLeftWord(info);
info; // {words: {remainingLeft: ['world'], matchedLeft: ['hello']}}
```
#### `completion.matchLeftWord(leftWord, words)`
Helper function to find words from `words` that start with `leftWord`
- leftWord `String` - Word to match left content of
- `leftWord` gets its name from usually coming from `words.partialLeft`
- words `String[]` - Array of words to filter against
Returns:
- matchedWords `String[]` - Matching words from `words` that start with `leftWord`
```js
this.matchLeftWord('hello', ['hello-world', 'hello-there', 'goodbye-moon']);
// ['hello-world', 'hello-there'];
```
#### `completion.complete(params, cb)`
Get potential completion matches for given parameters
- params `Object` - Information similar to that passed in by `bash's` tab completion

@@ -110,2 +160,10 @@ - line `String` - Input to complete against (similar to `COMP_LINE`)

#### `completion.resolveInfo(info, cb)`
Recursively find matches against the `Completion's tree` with a given `info`
- info `Object` - CLI information provided by [twolfson/line-info][]
- This is converted from `params` to its current equivalent by [twolfson/line-info][]
- cb `Function` - Error first callback function that receives matches
- `cb` should be the same as in `completion.complete`
## Examples

@@ -116,16 +174,35 @@ An example of `git` would be

var gitCompletion = new Completion({
git: {
name: 'git',
options: [{
// `git --help`, a terminal option
name: '--help'
}],
commands: [{
// `git checkout master`
checkout: function (info, cb) {
name: 'checkout',
option: [{
// `git checkout -b dev/hello`
name: '-b',
completion: function (info, cb) {
// `-b` was matched by `completion` so keep on recursing
return this.resolveInfo(info, cb);
}
}],
completion: function getGitBranches (info, cb) {
// Get git branches and find matches
},
remote: {
}
}, {
name: 'remote',
commands: [{
// `git remote add origin git@github.com:...`
add: null, // No possible tab completion here
// No possible completion here
name: 'add'
}, {
// `git remote rm origin`
rm: function (info, cb) {
name: 'rm',
completion: function getGitBranches (info, cb) {
// Get git branches and find matches
}
}
}
}]
}]
});

@@ -143,3 +220,3 @@

// `git remote |`
line: 'git remote',
line: 'git remote ',
cursor: 11

@@ -146,0 +223,0 @@ }, function (err, results) {

// Load in dependencies
var assert = require('assert');
var deepClone = require('clone');
var Completion = require('../');
var cursorUtils = require('./utils/cursor');
var completionUtils = require('./utils/completion');
// Define set of utilities for `completion`
var completionUtils = {
completeCommand: function (command) {
before(function (done) {
var params = cursorUtils.splitAtCursor(command);
var that = this;
this.completion.complete(params, function (err, results) {
that.results = results;
done(err);
});
});
after(function () {
delete this.results;
});
},
init: function (params) {
before(function initCompletion () {
this.completion = new Completion(params);
});
after(function cleanupCompletion () {
delete this.completion;
});
}
};
// Start our tests
describe('A partial command with one completion match', function () {
completionUtils.init({
npm: {
publish: null
}
name: 'npm',
commands: [{
name: 'publish'
}]
});

@@ -50,10 +25,14 @@

completionUtils.init({
git: {
checkout: function (params, cb) {
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello.world']);
},
'cherry-pick': function (params, cb) {
}
}, {
name: 'cherry-pick',
completion: function (info, cb) {
cb(null, ['maraschino']);
}
}
}]
});

@@ -72,10 +51,14 @@

completionUtils.init({
git: {
checkout: function (params, cb) {
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello.world']);
},
'cherry-pick': function (params, cb) {
}
}, {
name: 'cherry-pick',
completion: function (info, cb) {
cb(null, ['maraschino']);
}
}
}]
});

@@ -94,5 +77,6 @@

completionUtils.init({
npm: {
publish: null
}
name: 'npm',
commands: [{
name: 'publish'
}]
});

@@ -110,6 +94,7 @@

describe('A terminal command with whitespace', function () {
completionUtils.init({
npm: {
publish: null
}
completionUtils.init({
name: 'npm',
commands: [{
name: 'publish'
}]
});

@@ -128,7 +113,9 @@

completionUtils.init({
git: {
checkout: function (params, cb) {
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}
}]
});

@@ -147,7 +134,9 @@

completionUtils.init({
git: {
checkout: function (params, cb) {
name: 'git',
commands: [{
name: 'checkout',
completion: function (info, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}
}]
});

@@ -164,39 +153,20 @@

describe.skip('A command with options', function () {
describe('A many level command', function () {
completionUtils.init({
git: {
'-b': function (params, cb) {
// The `-b` has already been shifted because we matched `-b`
// As a result, attempt to complete once again from `git's` context
this.parentNode.completeInfo(params, cb);
},
checkout: function (params, cb) {
cb(null, ['hello-world', 'hello-there']);
}
}
name: 'git',
commands: [{
name: 'remote',
commands: [{
name: 'add'
}]
}]
});
describe('being completed with a terminal command', function () {
completionUtils.completeCommand('git -b chec|');
describe('when completing an incomplete command', function () {
completionUtils.completeCommand('git remote a|');
it('returns the command without completion options', function () {
assert.deepEqual(this.results, ['checkout']);
it('returns the expected command', function () {
assert.deepEqual(this.results, ['add']);
});
});
describe('being completed with a non-terminal command', function () {
completionUtils.completeCommand('git -b checkout hello|');
it('returns the command without completion options', function () {
assert.deepEqual(this.results, ['hello-world', 'hello-there']);
});
});
describe('being completed with a command followed by an option', function () {
completionUtils.completeCommand('git checkout -b wat|');
it('returns the command without completion options', function () {
assert.deepEqual(this.results, []);
});
});
});