From 1951c736c81d2452cc23dab8dc3c67bc95de7925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raymond=20Maug=C3=A9?= Date: Tue, 28 Jun 2022 09:14:05 -0700 Subject: [PATCH 1/2] feat: adds plugin feature for extending cli with new commands --- bin/ask.js | 28 ++- lib/utils/plugin-utils.js | 170 ++++++++++++++++++ test/fixture/pluginCommands/ask-new | 0 test/fixture/pluginCommands/ask-sample | 0 .../pluginCommands/duplicate/ask-sample | 0 test/functional/commands/plugins-test.js | 38 ++++ test/functional/run-test.js | 3 +- test/test-utils.js | 11 +- 8 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 lib/utils/plugin-utils.js create mode 100755 test/fixture/pluginCommands/ask-new create mode 100755 test/fixture/pluginCommands/ask-sample create mode 100755 test/fixture/pluginCommands/duplicate/ask-sample create mode 100644 test/functional/commands/plugins-test.js diff --git a/bin/ask.js b/bin/ask.js index 411e5942..1a4e8522 100755 --- a/bin/ask.js +++ b/bin/ask.js @@ -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); @@ -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); -} +} \ No newline at end of file diff --git a/lib/utils/plugin-utils.js b/lib/utils/plugin-utils.js new file mode 100644 index 00000000..0bfc6673 --- /dev/null +++ b/lib/utils/plugin-utils.js @@ -0,0 +1,170 @@ +const fs = require('node:fs'); +const path = require('node: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}`); + } + }); + } +} \ No newline at end of file diff --git a/test/fixture/pluginCommands/ask-new b/test/fixture/pluginCommands/ask-new new file mode 100755 index 00000000..e69de29b diff --git a/test/fixture/pluginCommands/ask-sample b/test/fixture/pluginCommands/ask-sample new file mode 100755 index 00000000..e69de29b diff --git a/test/fixture/pluginCommands/duplicate/ask-sample b/test/fixture/pluginCommands/duplicate/ask-sample new file mode 100755 index 00000000..e69de29b diff --git a/test/functional/commands/plugins-test.js b/test/functional/commands/plugins-test.js new file mode 100644 index 00000000..2ec9ff38 --- /dev/null +++ b/test/functional/commands/plugins-test.js @@ -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'); + }); +}); \ No newline at end of file diff --git a/test/functional/run-test.js b/test/functional/run-test.js index b1ea8f82..fa42f11e 100644 --- a/test/functional/run-test.js +++ b/test/functional/run-test.js @@ -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); diff --git a/test/test-utils.js b/test/test-utils.js index c53ad67c..52146891 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -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); @@ -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; @@ -114,5 +122,6 @@ module.exports = { run, startMockSmapiServer, startMockLwaServer, - MockServerPort + MockServerPort, + addFixtureDirectoryToPaths }; From 6b1b7066737a5ada14039cfe7a858543d52e94e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raymond=20Maug=C3=A9?= Date: Thu, 30 Jun 2022 10:45:38 -0700 Subject: [PATCH 2/2] Removes module prefix for Node 12 --- lib/utils/plugin-utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utils/plugin-utils.js b/lib/utils/plugin-utils.js index 0bfc6673..7c1c38a8 100644 --- a/lib/utils/plugin-utils.js +++ b/lib/utils/plugin-utils.js @@ -1,5 +1,5 @@ -const fs = require('node:fs'); -const path = require('node:path'); +const fs = require('fs'); +const path = require('path'); const Messenger = require('@src/view/messenger'); @@ -167,4 +167,4 @@ function reportDuplicateCommands(coreCommands, subCommands, duplicateCommands) { } }); } -} \ No newline at end of file +}