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 6, 2020
1 parent c11beb8 commit 1105c06
Show file tree
Hide file tree
Showing 15 changed files with 446 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,21 +9,25 @@ 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('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', '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.');
}
29 changes: 29 additions & 0 deletions docs/concepts/Autocompletion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Autocompletion

## Prerequisites

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

For bash, please install bash-completion.

```
brew install bash-completion
```

## 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
102 changes: 102 additions & 0 deletions lib/commands/autocomplete/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const fs = require('fs-extra');

const CONSTANTS = require('@src/utils/constants');

module.exports = class Helper {
constructor(omelette, commanders = []) {
this.commanders = commanders;
this.completion = omelette(`ask <command> <subCommand> ${'<option> '.repeat(50).trim()}`);
this.autoCompleteHintsFile = 'autocomplete-hints.json';
}

_mapOptionsToParent(options, parent) {
// default options
parent['-h'] = {};
parent['--help'] = {};
options.forEach(opt => {
const { long, short } = opt;
// there is always long name
parent[long] = {};
if (short) {
parent[short] = {};
}
});
}

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

return options;
}

_parseArguments(line) {
const [ask, command, sumCommand] = line.split(' ').map(i => i.trim());
return { ask, command, sumCommand };
}

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

this.completion
.on('command', ({ reply }) => {
reply(CONSTANTS.TOP_LEVEL_COMMANDS);
})
.on('subCommand', ({ line, reply }) => {
const { command } = this._parseArguments(line);
if (options[command]) {
reply(Object.keys(options[command]));
}
})
.on('option', ({ line, reply }) => {
const { command, sumCommand } = this._parseArguments(line);
if (options[command] && options[command][sumCommand]) {
reply(Object.keys(options[command][sumCommand]));
}
});

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());
}
};
47 changes: 47 additions & 0 deletions lib/commands/autocomplete/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 helper = new Helper(omelette, commanders);
const program = new commander.Command();
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', '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

0 comments on commit 1105c06

Please sign in to comment.