Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add autocomplete #333

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my biggest question would be can we save this registry step each time a new command is created? I know this is a subcommand, but is it still possible to load from the main commander object? Based on the current cmd structure, we only need it for ask ,ask smapi, ask utils, ask skill. Is it possible to iterate the command list in bin folder, and dynamically load them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot think of a way. Do you know a way?


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);
}
39 changes: 39 additions & 0 deletions docs/concepts/Autocompletion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Autocompletion

## Prerequisites

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

### Bash prerequisites setup

1. Install bash-completion.

```
brew install bash-completion
```

2. Add bash_completion to ~/.bash_profile or ~/.bashrc:

```
echo '[[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]] && . "/usr/local/etc/profile.d/bash_completion.sh"' >> ~/.bash_profile
```

Similar prerequisites steps can be run for zsh and fish shells.

## 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;
RonWang marked this conversation as resolved.
Show resolved Hide resolved
} 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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor, should this func be renamed to _getAutoCompleteCmd?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cmd? it just loads auto complete options tree.
{
smapi: [commandOne, commandTwo...]
...
}

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) {
RonWang marked this conversation as resolved.
Show resolved Hide resolved
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

II think we need a try catch to cover this setupShellInitFile function, as suggested by the user https://github.com/f/omelette#automated-install. This step has too many potential problems depending on user's machine.

We need to do the cleanup if it fails, and pop up the message for the failure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What type of clean up? How can we know what needs to be cleaned up? If this steps fails it will show user the error message from omelette. What extra can we do in try and catch except of rethrowing the error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When i run in windows here is the result:

PS D:\Github\ask-cli> ask autocomplete setup
D:\Github\ask-cli\node_modules\omelette\src\omelette.js:189
          throw new Error('Shell could not be detected');
          ^

Error: Shell could not be detected
    at Omelette.getActiveShell (D:\Github\ask-cli\node_modules\omelette\src\omelette.js:189:17)
    at Omelette.getDefaultShellInitFile (D:\Github\ask-cli\node_modules\omelette\src\omelette.js:210:35)
    at Omelette.setupShellInitFile (D:\Github\ask-cli\node_modules\omelette\src\omelette.js:238:42)
    at D:\Github\ask-cli\lib\commands\autocomplete\helper.js:52:61
    at Helper._withProcessExitDisabled (D:\Github\ask-cli\lib\commands\autocomplete\helper.js:35:9)
    at Helper.setUpAutoComplete (D:\Github\ask-cli\lib\commands\autocomplete\helper.js:52:14)
    at Command.<anonymous> (D:\Github\ask-cli\lib\commands\autocomplete\index.js:29:20)
    at Command.listener (D:\Github\ask-cli\node_modules\commander\index.js:370:29)
    at Command.emit (events.js:314:20)
    at Command.EventEmitter.emit (domain.js:486:12)

Should we pre-check the OS? And give a good message if it falls through?

}

/**
* 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';
kakhaUrigashvili marked this conversation as resolved.
Show resolved Hide resolved
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"
RonWang marked this conversation as resolved.
Show resolved Hide resolved
},
"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();
});
});