Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions bin/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ if (!require('semver').gte(process.version, '8.3.0')) {
}

require('module-alias/register');

const commander = require('commander');
const pluginUtils = require('@src/utils/plugin-utils');

require('@src/commands/configure').createCommand(commander);
require('@src/commands/deploy').createCommand(commander);
Expand All @@ -22,12 +24,28 @@ commander
.command('smapi', 'list of Alexa Skill Management API commands')
.command('skill', 'increase the productivity when managing skill metadata')
.command('util', 'tooling functions when using ask-cli to manage Alexa Skill')
.version(require('../package.json').version)
.parse(process.argv);
.version(require('../package.json').version);

const ALLOWED_ASK_ARGV_2 = ['-V', '--version', ' - h', '--help'];

let coreCommands = commander.commands.map(command => ({ name: command._name }));

coreCommands.forEach(subCommand => ALLOWED_ASK_ARGV_2.push(subCommand.name));

let pluginResults = pluginUtils.findPluginsInEnvPath(coreCommands);

if (pluginResults.subCommands.length > 0) {
pluginUtils.addCommands(commander, pluginResults.subCommands);
pluginResults.subCommands.forEach(subCommand => ALLOWED_ASK_ARGV_2.unshift(subCommand.name));
}

if (pluginResults.duplicateCommands.length > 0) {
pluginUtils.reportDuplicateCommands(coreCommands, pluginResults.subCommands, pluginResults.duplicateCommands);
}

commander.parse(process.argv);

const ALLOWED_ASK_ARGV_2 = ['configure', 'deploy', 'new', 'init', 'dialog', 'smapi', 'skill', 'util', 'help', '-v',
'--version', '-h', '--help', 'run'];
if (process.argv[2] && ALLOWED_ASK_ARGV_2.indexOf(process.argv[2]) === -1) {
console.log('Command not recognized. Please run "ask" to check the user instructions.');
process.exit(1);
}
}
170 changes: 170 additions & 0 deletions lib/utils/plugin-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const fs = require('fs');
const path = require('path');

const Messenger = require('@src/view/messenger');

/**
* @typedef CommandResult
* @type {object}.
* @property {string} name -- command name
* @property {string} path - command executable path if any
*
* @typedef PluginResults
* @type {object}.
* @property {CommandResult[]} subCommands -- command name
* @property {CommandResult[]} duplicateCommands - command executable path if any
*/

module.exports = {
findPluginSubcommands: findPluginSubcommands,
findPluginsInEnvPath: findPluginsInEnvPath,
addCommands: addCommands,
reportDuplicateCommands: reportDuplicateCommands
};

/**
* Searches a directory for all subcommands (identified by naming convention 'parentCommandName-subcommandName')
* Does not search sub-directories
* @param {string} parentCommandName -- parent command name
* @param {string} path -- search path
* @param {CommandResult[]} existingCommands -- commands that have been previously identified
* @returns {PluginResults}
*/
function findPluginSubcommands(parentCommandName, currentPath, existingCommands) {

const subCommands = [];
const duplicateCommands = [];

if (fs.existsSync(currentPath)) {
const stats = fs.lstatSync(currentPath);

if (stats.isDirectory()) {
const contents = fs.readdirSync(currentPath, { withFileTypes: true });

function resolveSubCommand(entry) {
let fullPath = path.join(currentPath, entry.name);

if (entry.isFile() || entry.isSymbolicLink()) {
const fileName = path.basename(fullPath, path.extname(fullPath));
let [commandPrefix, commandName] = fileName.split('-');

if (commandPrefix &&
commandPrefix == parentCommandName &&
commandName) {

if (entry.isSymbolicLink()) {
let realPath = fs.realpathSync(fullPath);
let realPathStat = fs.statSync(realPath, { throwIfNoEntry: false });

if (!realPathStat.isFile()) {
return;
}
}

let duplicateCommand = existingCommands.find((command) => {
return command.name === commandName;
});

if (!duplicateCommand) {
subCommands.push(
{
name: commandName,
path: fullPath
}
);
} else {
duplicateCommands.push(
{
name: commandName,
path: fullPath
}
);
}
}
}
}

contents.forEach(resolveSubCommand);

} else {
Messenger.getInstance().warn(`'${currentPath}' is not a directory`);
}
} else {
Messenger.getInstance().warn(`directory '${currentPath}' could not be found`);
}

return {
subCommands: subCommands,
duplicateCommands: duplicateCommands
};
}

/**
* Find plugins on user env PATH
* @param {CommandResult[]} existingCommands -- commands that have been previously identified
* @returns {PluginResults}
*/
function findPluginsInEnvPath(existingCommands) {

let pluginResults = {
subCommands: [],
duplicateCommands: []
};

existingCommands = existingCommands.slice();
if (process.env.PATH) {
let paths = process.env.PATH.split(path.delimiter);

paths.forEach((path) => {
let results;
results = findPluginSubcommands('ask', path, existingCommands);

if (results.subCommands) {
pluginResults.subCommands = pluginResults.subCommands.concat(results.subCommands);
existingCommands = existingCommands.concat(results.subCommands);
}

if (results.duplicateCommands) {
pluginResults.duplicateCommands = pluginResults.duplicateCommands.concat(results.duplicateCommands);
}
});
}

return pluginResults;
}

/**
* Adds subcommands to a command
* @param {*} commander
* @param {CommandResult[]} subCommands
*/
function addCommands(commander, subCommands) {
if (subCommands) {
subCommands.forEach((subCommand) => {
Messenger.getInstance().info(`Found plugin: ${subCommand.name} plugin, location: ${subCommand.path}`);
commander.command(subCommand.name, `${subCommand.name} (plugin)`, { executableFile: `${subCommand.path}` });
});
}
}

/**
* Outputs warning of duplcate commands
* @param {*} commander
* @param {CommandResult[]} coreCommands
* @param {CommandResult[]} duplicateCommands
*
*/
function reportDuplicateCommands(coreCommands, subCommands, duplicateCommands) {
if (duplicateCommands) {
duplicateCommands.forEach((duplicateCommand) => {
let found = coreCommands.find(command => command.name === duplicateCommand.name);

if (found) { // look in core commands first
Messenger.getInstance().info(`${duplicateCommand.path} is overshadowed by a core command wth the same name: ${found.name}`);
} else {
found = subCommands.find(command => command.name === duplicateCommand.name);
Messenger.getInstance().info(`${duplicateCommand.path} is overshadowed by a plugin with the same name: ${found.path}`);
}
});
}
}
Empty file.
Empty file.
Empty file.
38 changes: 38 additions & 0 deletions test/functional/commands/plugins-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { expect } = require('chai');
const parallel = require('mocha.parallel');
const sinon = require('sinon');
const { run, addFixtureDirectoryToPaths } = require('@test/test-utils');

parallel('plugin test', () => {

const sandbox = sinon.createSandbox();

beforeEach(() => {
cmd = 'ask';
let env_path = process.env.PATH;

env_path = addFixtureDirectoryToPaths(env_path);

sandbox.stub(process.env, 'PATH').value(env_path);
});

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

it('| should warn of attempt to override core command', async () => {

let args = []
const result = await run(cmd, args);

expect(result).include('ask-new is overshadowed by a core command');
});

it('| should warn of duplicate command', async () => {

let args = [];
const result = await run(cmd, args);

expect(result).include('ask-sample is overshadowed by a plugin with the same name');
});
});
3 changes: 2 additions & 1 deletion test/functional/run-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ require('module-alias/register');
process.env.ASK_SHARE_USAGE = false;

[
'@test/functional/commands/high-level-commands-test.js'
'@test/functional/commands/high-level-commands-test.js',
'@test/functional/commands/plugins-test.js',
].forEach((testFile) => {
// eslint-disable-next-line global-require
require(testFile);
Expand Down
11 changes: 10 additions & 1 deletion test/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const MockServerPort = {

const tempDirectory = path.join(process.cwd(), 'test/temp');

const fixturePluginDirectory = path.join(process.cwd(), 'test/fixture/pluginCommands');

const resetTempDirectory = () => {
fs.ensureDirSync(tempDirectory);
fs.emptyDirSync(tempDirectory);
Expand All @@ -34,6 +36,12 @@ const makeFolderInTempDirectory = (folderPath) => {
return fullPath;
};

const addFixtureDirectoryToPaths = (envPath) => {
var pluginDir = fixturePluginDirectory;
var pluginDuplicatesDir = path.join(fixturePluginDirectory, "duplicate");
return pluginDir + path.delimiter + pluginDuplicatesDir + path.delimiter + envPath;
}

const run = (cmd, args, options = {}) => {
const inputs = options.inputs || [];
const parse = options.parse || false;
Expand Down Expand Up @@ -114,5 +122,6 @@ module.exports = {
run,
startMockSmapiServer,
startMockLwaServer,
MockServerPort
MockServerPort,
addFixtureDirectoryToPaths
};