Skip to content

Commit

Permalink
feat: add autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
kakha urigashvili committed Nov 11, 2020
1 parent 114b01e commit 06b4833
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ coverage
# test temp
test/temp

autocomplete-hints.json

29 changes: 29 additions & 0 deletions bin/ask-autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env node

require('module-alias/register');
const commander = require('commander');
const { makeAutoCompleteCommander } = require('@src/commands/autocomplete');
const { makeSmapiCommander } = require('@src/commands/smapi/smapi-commander');
const ConfigureCommander = require('@src/commands/configure');
const DeployCommander = require('@src/commands/deploy');
const DialogCommander = require('@src/commands/dialog');
const InitCommander = require('@src/commands/init');
const NewCommander = require('@src/commands/new');
const UtilCommander = require('@src/commands/util/util-commander');

const smapiCommander = makeSmapiCommander();
const utilCommander = UtilCommander.commander;
const configureCommander = ConfigureCommander.createCommand(commander);
const deployCommander = DeployCommander.createCommand(commander);
const newCommander = NewCommander.createCommand(commander);
const initCommander = InitCommander.createCommand(commander);
const dialogCommander = DialogCommander.createCommand(commander);
const commanders = [smapiCommander, utilCommander, configureCommander, deployCommander, newCommander, initCommander, dialogCommander];

const autoCompleteCommander = makeAutoCompleteCommander(commanders);

if (!process.argv.slice(2).length) {
autoCompleteCommander.outputHelp();
} else {
autoCompleteCommander.parse(process.argv);
}
8 changes: 6 additions & 2 deletions bin/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,27 @@ if (!require('semver').gte(process.version, '8.3.0')) {

require('module-alias/register');
const commander = require('commander');
const CONSTANTS = require('@src/utils/constants');
const { initAutoComplete } = require('@src/commands/autocomplete');

require('@src/commands/configure').createCommand(commander);
require('@src/commands/deploy').createCommand(commander);
require('@src/commands/new').createCommand(commander);
require('@src/commands/init').createCommand(commander);
require('@src/commands/dialog').createCommand(commander);

initAutoComplete();

commander
.description('Command Line Interface for Alexa Skill Kit')
.command('smapi', 'list of Alexa Skill Management API commands')
.command('skill', 'increase the productivity when managing skill metadata')
.command('autocomplete', 'sets up terminal auto completion')
.command('util', 'tooling functions when using ask-cli to manage Alexa Skill')
.version(require('../package.json').version)
.parse(process.argv);

const ALLOWED_ASK_ARGV_2 = ['configure', 'deploy', 'new', 'init', 'dialog', 'smapi', 'skill', 'util', 'help', '-v', '--version', '-h', '--help'];
if (process.argv[2] && ALLOWED_ASK_ARGV_2.indexOf(process.argv[2]) === -1) {
if (process.argv[2] && CONSTANTS.TOP_LEVEL_COMMANDS.indexOf(process.argv[2]) === -1) {
console.log('Command not recognized. Please run "ask" to check the user instructions.');
process.exit(1);
}
35 changes: 35 additions & 0 deletions docs/concepts/Autocompletion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Autocompletion

## Prerequisites

Autocompletion currently works for the following shells: bash, zsh and fish.

For bash, please install bash-completion.

```
brew install bash-completion
```

And the then add the following line to ~/.bash_profile or ~/.bashrc:

```
[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"
```

## Enable Autocompletion
To setup auto completion, please run the following command and then restart the terminal.

```
ask autocomplete setup
```


## Disable Autocompletion
To disable auto completion, please run the following command and then restart the terminal.

```
ask autocomplete cleanup
```



1 change: 1 addition & 0 deletions lib/commands/abstract-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class AbstractCommand {

// register command action
this._registerAction(commanderCopy);
return commanderCopy;
} catch (err) {
Messenger.getInstance().fatal(err);
this.exit(1);
Expand Down
61 changes: 61 additions & 0 deletions lib/commands/autocomplete/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const fs = require('fs-extra');
const path = require('path');

module.exports = class Helper {
constructor(omelette, commanders = []) {
this.commanders = commanders;
this.completion = omelette('ask');
this.autoCompleteHintsFile = path.join(__dirname, 'autocomplete-hints.json');
}

_getAutoCompleteOptions() {
const options = {};
this.commanders.forEach(com => {
options[com.name()] = com.commands.map(sumCom => sumCom.name());
});

return options;
}

/**
* Initializes auto complete inside of the program
*/
initAutoComplete() {
if (fs.existsSync(this.autoCompleteHintsFile)) {
const options = fs.readJsonSync(this.autoCompleteHintsFile);

this.completion.tree(options);
this.completion.init();
}
}

_withProcessExitDisabled(fn) {
const origExit = process.exit;
process.exit = () => {};
fn();
process.exit = origExit;
}

/**
* Regenerates auto complete hints file
*/
reloadAutoCompleteHints() {
const options = this._getAutoCompleteOptions();
fs.writeJSONSync(this.autoCompleteHintsFile, options);
}

/**
* Sets ups auto complete. For example, adds autocomplete entry to .bash_profile file
*/
setUpAutoComplete() {
this.reloadAutoCompleteHints();
this._withProcessExitDisabled(() => this.completion.setupShellInitFile());
}

/**
* Removes auto complete. For example, removes autocomplete entry from .bash_profile file
*/
cleanUpAutoComplete() {
this._withProcessExitDisabled(() => this.completion.cleanupShellInitFile());
}
};
50 changes: 50 additions & 0 deletions lib/commands/autocomplete/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const commander = require('commander');
const omelette = require('omelette');
const Messenger = require('@src/view/messenger');
const Helper = require('./helper');

/**
* Initializes auto complete inside of the program
*/
const initAutoComplete = () => {
const helper = new Helper(omelette);
helper.initAutoComplete();
};
/**
* Creates auto complete commander
* @param {*} commanders list of commanders used for creating an autocomplete hints file
*/
const makeAutoCompleteCommander = commanders => {
const program = new commander.Command();
commanders.push(program);

const helper = new Helper(omelette, commanders);

program._name = 'autocomplete';
program.description('sets up ask cli terminal auto completion');

program.command('setup')
.description('set up auto completion')
.action(() => {
helper.setUpAutoComplete();
Messenger.getInstance().info('Successfully set up auto completion. Please, reload the terminal.');
});

program.command('cleanup')
.description('clean up auto completion')
.action(() => {
helper.cleanUpAutoComplete();
Messenger.getInstance().info('Successfully removed auto completion. Please, reload the terminal.');
});

program.command('reload')
.description('regenerates hints file')
.action(() => {
helper.reloadAutoCompleteHints();
Messenger.getInstance().info('Successfully regenerated the hints file.');
});

return program;
};

module.exports = { initAutoComplete, makeAutoCompleteCommander };
2 changes: 1 addition & 1 deletion lib/commands/smapi/smapi-commander.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const makeSmapiCommander = () => {
getTask.createCommand(program);
searchTask.createCommand(program);

program._name = 'ask smapi';
program._name = 'smapi';
program
.description('The smapi command provides a number of sub-commands that '
+ 'enable you to manage Alexa skills associated with your developer account.');
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/util/util-commander.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Object.keys(UTIL_COMMAND_MAP).forEach((cmd) => {
require(UTIL_COMMAND_MAP[cmd]).createCommand(commander);
});

commander._name = 'ask util';
commander._name = 'util';
commander
.description('tooling functions when using ask-cli to manage Alexa Skill');

Expand Down
3 changes: 3 additions & 0 deletions lib/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ module.exports.METRICS = {
ENDPOINT: 'https://client-telemetry.amazonalexa.com'
};

module.exports.TOP_LEVEL_COMMANDS = ['configure', 'deploy', 'new', 'init', 'dialog', 'smapi', 'skill', 'util', 'help',
'autocomplete', '-v', '--version', '-h', '--help'];

module.exports.DEPLOYER_TYPE = {
HOSTED: {
OPTION_NAME: 'Alexa-hosted skills',
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"prepublishOnly": "babel lib -d lib; babel bin -d bin",
"pre-release": "standard-version",
"prism": "prism",
"postinstall": "node postinstall.js"
"postinstall": "node bin/ask-autocomplete.js reload; node postinstall.js"
},
"dependencies": {
"adm-zip": "^0.4.13",
Expand All @@ -58,6 +58,7 @@
"listr": "^0.14.3",
"module-alias": "^2.1.0",
"mustache": "^4.0.1",
"omelette": "^0.4.15-1",
"open": "^7.0.3",
"ora": "^3.4.0",
"portscanner": "^2.1.1",
Expand Down
86 changes: 86 additions & 0 deletions test/unit/commands/autocomplete/helper-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { expect } = require('chai');
const commander = require('commander');
const EventEmitter = require('events');
const fs = require('fs-extra');
const sinon = require('sinon');

const Helper = require('@src/commands/autocomplete/helper');

describe('Commands autocomplete - helper test', () => {
let helper;
let setupShellInitFileStub;
let cleanupShellInitFileStub;
let initStub;
let treeStub;
let omeletteStub;

const testCommander = new commander.Command();
testCommander._name = 'test';
testCommander.command('command-one');
testCommander.command('command-two');

const commanders = [testCommander];

beforeEach(() => {
setupShellInitFileStub = sinon.stub();
cleanupShellInitFileStub = sinon.stub();
initStub = sinon.stub();
treeStub = sinon.stub();

omeletteStub = () => {
class OmeletteStubClass extends EventEmitter {
constructor() {
super();
this.setupShellInitFile = setupShellInitFileStub;
this.cleanupShellInitFile = cleanupShellInitFileStub;
this.init = initStub;
this.tree = treeStub;
}
}
return new OmeletteStubClass();
};

helper = new Helper(omeletteStub, commanders);
});

it('should set up autocomplete', () => {
const writeJSONStub = sinon.stub(fs, 'writeJSONSync');
helper.setUpAutoComplete();

expect(writeJSONStub.callCount).eq(1);
expect(setupShellInitFileStub.callCount).eq(1);
});

it('should regenerate autocomplete hints file', () => {
const writeJSONStub = sinon.stub(fs, 'writeJSONSync');
helper.reloadAutoCompleteHints();

expect(writeJSONStub.callCount).eq(1);
});

it('should clean up autocomplete', () => {
helper.cleanUpAutoComplete();

expect(cleanupShellInitFileStub.callCount).eq(1);
});

it('should not initialize autocomplete if hint file is not present', () => {
sinon.stub(fs, 'existsSync').withArgs(helper.autoCompleteHintsFile).returns(false);
helper.initAutoComplete();

expect(initStub.callCount).eq(0);
});

it('initialize autocomplete if hint file is present', () => {
sinon.stub(fs, 'existsSync').withArgs(helper.autoCompleteHintsFile).returns(true);
sinon.stub(fs, 'readJsonSync').withArgs(helper.autoCompleteHintsFile).returns({});
helper.initAutoComplete();

expect(treeStub.callCount).eq(1);
expect(initStub.callCount).eq(1);
});

afterEach(() => {
sinon.restore();
});
});

0 comments on commit 06b4833

Please sign in to comment.