diff --git a/bin/common/helper/delayer.js b/bin/common/helper/delayer.js index e00e6a7..0ed1b59 100644 --- a/bin/common/helper/delayer.js +++ b/bin/common/helper/delayer.js @@ -21,14 +21,14 @@ class Delayer { /** * Message to be included with promise when delay ends - * @var {string | boolean} + * @var {unknown} */ #value; /** * * @param ms {number} Milliseconds until delay ends - * @param value {string | boolean} @Optional include with promise when delay is expired + * @param value {unknown} @Optional include with promise when delay is expired * @returns {Promise} */ async delay(ms, value) { @@ -38,7 +38,7 @@ class Delayer { /** * Will stop delay before expiration - * @param value {string | boolean} @Optional Value will be included with promise + * @param value {unknown} @Optional Value will be included with promise * @return {void} */ done(value) { @@ -55,7 +55,7 @@ class Delayer { /** * Sets message that will be included when delay promise ends - * @param value {string | boolean | null} + * @param value {unknown} * @return {void} */ #setValue(value) { diff --git a/bin/configuration/handleComponents.js b/bin/configuration/handleComponents.js index 8c62ac2..cc46362 100644 --- a/bin/configuration/handleComponents.js +++ b/bin/configuration/handleComponents.js @@ -19,7 +19,7 @@ function get() { } return { - components: config.components.map(x => config_handler.getComponentDeverJsonConfig(x)) + components: config.components.map(x => config_handler.getComponentConfig(x)) } } diff --git a/bin/configuration/handleConfigFile.js b/bin/configuration/handleConfigFile.js index dd3aea2..76353c0 100644 --- a/bin/configuration/handleConfigFile.js +++ b/bin/configuration/handleConfigFile.js @@ -1,257 +1,90 @@ -module.exports = { - get: get, - getComponentDeverJsonConfig: getComponentDeverJsonConfig, - write: write -} - const path = require("path"); const fs = require("fs"); -const fileName = 'dever_config.json'; - -const root = path.join(path.dirname(fs.realpathSync(__filename)), '../'); -const filePath = path.join(root, fileName); - -function readJson(filePath) { - try { - let rawData = fs.readFileSync(filePath); - return JSON.parse(rawData); - } - catch (e) { - switch (e.code) { - case "ENOENT": - console.error(`Could not find '${filePath}' please run 'dever init' again.`); - return null; - default: - throw e; - } - } -} - -/** - * - * @returns {LocalConfig} - */ -function get() { - const config = readJson(filePath); - - if (config == null) { - throw 'Could not find configuration'; - } - - return config; -} - -/** - * Save configuration - * @param config {LocalConfig} - */ -function write(config) { - const fs = require('fs'); - let data = JSON.stringify(config); - - fs.writeFileSync(filePath, data, (err) => { - if (err) { - throw err; - } +module.exports = new class { + #fileName = 'dever_config.json'; - console.log(data); - console.log('Configuration updated'); - }); -} + #root; + #filePath; -/** - * Get component with location - * @param filePath - * @returns {null|Config} - */ -function getComponentDeverJsonConfig(filePath) { - const component = readJson(filePath); - if (component == null) { - return null; + constructor() { + this.#root = path.join(path.dirname(fs.realpathSync(__filename)), '../'); + this.#filePath = path.join(this.#root, this.#fileName); } - component['location'] = path.dirname(filePath); - - return component; -} - -class LocalConfig { /** - * @return {Config[]} + * Save configuration + * @param config {LocalConfig} */ - components; -} - -class Config { - /** - * @return {string} - */ - version; + write(config) { + const fs = require('fs'); + let data = JSON.stringify(config); - /** - * @return {string} - */ - component; - - /** - * @return {string[]} - */ - keywords; - - /** - * @return {Dependency[]} - */ - dependencies; - - /** - * @return {string} - */ - location; -} - -class Dependency { - /** - * Define which handler you're using ('docker-container','powershell-command','powershell-script','docker-compose','mssql') - * @return {string} - */ - type; - - /** - * @return {string} - */ - name; - - /** - * @return {string} - */ - file; - - /** - * @return {string} - */ - command; - - /** - * Container object only used when type is 'docker-container' - * @return {Container | null} - */ - container; + fs.writeFileSync(this.#filePath, data, (err) => { + if (err) { + throw err; + } - /** - * Currently only used to select between mssql options - * @return {string | null} - */ - option; - - /** - * Custom options that will be passed along to dependency - * @return {CustomOption[] | null} - */ - options; - // Todo: Consider new name for this property - - /** - * @return {Wait} - */ - wait; - - /** - * Informs whether a dependency needs to be run as elevated user - * @return {boolean} - */ - runAsElevated; -} - -class Wait { - /** - * Choose when wait should occur ('before', 'after') - * @return {string} - */ - when; - - /** - * Choose for how long it should wait - * @return {number} - */ - time; // in milliseconds -} - -class Container { - /** - * Name - * @var {string} - */ - name; - - /** - * Port mappings - * @var {string[]} - */ - ports; - - /** - * Environment variables - * @var {string[]} - */ - variables; - - /** - * Name of docker image - * @var {string} - */ - image; -} - -class CustomOption { - /** - * Check if dependency is allowed to execute without option - * @return {boolean} - */ - required; + console.log(data); + console.log('Configuration updated'); + }); + } /** - * Option key can be used in console - * @return {string} + * Get dever configuration + * @returns {LocalConfig} */ - key; + get() { + const config = this.#readJson(this.#filePath); - /** - * Possibility for having an alias for the option - * @Optional - * @return {string} - */ - alias; + if (config == null) { + throw 'Could not find configuration'; + } - /** - * Describe what this option will be used for - * @return {string} - */ - describe; + return config; + } /** - * Replace specific area given in value area e.g. "$0" if e.g. command is "docker run $0 nginx" - * @return {string} + * Get all configuration for all components + * @returns {null|Config[]} */ - insert; + getAllComponentsConfig() { + const config = this.#readJson(this.#filePath); + return config == null ? + null : + config.components.map(x => this.#readJson(x)); + } /** - * Condition for which this option is allowed to receive a value - * @return {CustomOptionRule} + * Get component configuration + * @param filePath + * @returns {null|Config} */ - rule; -} + getComponentConfig(filePath) { + const component = this.#readJson(filePath); -class CustomOptionRule { - /** - * Check whether value being passed is as expected using regex match - * @return {string} - */ - match; + return component == null ? + null : + {...component, location: path.dirname(filePath)}; + } /** - * If condition check fails this message will be shown - * @return {string} - */ - message; + * Get and parse file + * @param filePath {string} + * @returns {null|LocalConfig|Config} + */ + #readJson(filePath) { + try { + let rawData = fs.readFileSync(filePath); + return JSON.parse(rawData); + } catch (e) { + switch (e.code) { + case "ENOENT": + console.error(`Could not find '${filePath}' please run 'dever init' again.`); + return null; + default: + throw e; + } + } + } } diff --git a/bin/configuration/handleFixConfig.js b/bin/configuration/handleFixConfig.js new file mode 100644 index 0000000..6925571 --- /dev/null +++ b/bin/configuration/handleFixConfig.js @@ -0,0 +1,56 @@ +const config_handler = require('./handleConfigFile'); + +module.exports = new class { + /** + * Get all fix commands + * @returns {null|Fix[]} + */ + getAll() { + const components = config_handler.getAllComponentsConfig(); + if (components == null) { + return null; + } + + const listOfFix = []; + for (const fixes of components.map(x => this.#addComponentToFix(x.fix, x.component))) { + if (fixes == null) { + continue; + } + + for (const fix in fixes) { + listOfFix.push({...fixes[fix], key: fix}); + } + } + + return listOfFix.length === 0 ? null : listOfFix; + } + + /** + * Get specific fix command + * @param problem {string} + * @returns {null|Fix[]} + */ + get(problem) { + const fix = this.getAll(); + if (fix == null) { + return null; + } + + return fix.filter(x => x.key === problem); + } + + /** + * Add component to each fix + * @param fixes {Fix[]} + * @param component {string} + * @returns {{}} + */ + #addComponentToFix(fixes, component) { + let objMerge = {}; + for (const key of Object.keys(fixes)) { + objMerge[key] = {...fixes[key], component}; + } + + return objMerge; + } +} \ No newline at end of file diff --git a/bin/configuration/models.js b/bin/configuration/models.js new file mode 100644 index 0000000..d1cde4c --- /dev/null +++ b/bin/configuration/models.js @@ -0,0 +1,216 @@ +class LocalConfig { + /** + * @return {Config[]} + */ + components; +} + +class Config { + /** + * @return {string} + */ + version; + + /** + * @return {string} + */ + component; + + /** + * @return {string[]} + */ + keywords; + + /** + * @return {Fix[]} + */ + fix; + + /** + * @return {Dependency[]} + */ + dependencies; + + /** + * @return {string} + */ + location; +} + +class Dependency { + /** + * Define which handler you're using ('docker-container','powershell-command','powershell-script','docker-compose','mssql') + * @return {string} + */ + type; + + /** + * @return {string} + */ + name; + + /** + * @return {string} + */ + file; + + /** + * @return {string} + */ + command; + + /** + * Container object only used when type is 'docker-container' + * @return {Container | null} + */ + container; + + /** + * Currently only used to select between mssql options + * @return {string | null} + */ + option; + + /** + * Custom options that will be passed along to dependency + * @return {CustomOption[] | null} + */ + options; + // Todo: Consider new name for this property + + /** + * @return {Wait} + */ + wait; + + /** + * Informs whether a dependency needs to be run as elevated user + * @return {boolean} + */ + runAsElevated; +} + +class Wait { + /** + * Choose when wait should occur ('before', 'after') + * @return {string} + */ + when; + + /** + * Choose for how long it should wait + * @return {number} + */ + time; // in milliseconds +} + +class Container { + /** + * Name + * @var {string} + */ + name; + + /** + * Port mappings + * @var {string[]} + */ + ports; + + /** + * Environment variables + * @var {string[]} + */ + variables; + + /** + * Name of docker image + * @var {string} + */ + image; +} + +class CustomOption { + /** + * Check if dependency is allowed to execute without option + * @return {boolean} + */ + required; + + /** + * Option key can be used in console + * @return {string} + */ + key; + + /** + * Possibility for having an alias for the option + * @Optional + * @return {string} + */ + alias; + + /** + * Describe what this option will be used for + * @return {string} + */ + describe; + + /** + * Replace specific area given in value area e.g. "$0" if e.g. command is "docker run $0 nginx" + * @return {string} + */ + insert; + + /** + * Condition for which this option is allowed to receive a value + * @return {CustomOptionRule} + */ + rule; +} + +class CustomOptionRule { + /** + * Check whether value being passed is as expected using regex match + * @return {string} + */ + match; + + /** + * If condition check fails this message will be shown + * @return {string} + */ + message; +} + +class Fix { + /** + * Name of the component which the fix is coming from + * @return {string} + */ + component; + + /** + * Keyword for fix command + * @return {string} + */ + key; + + /** + * Define which handler you're using ('powershell-command','powershell-script') + * @return {string} + */ + type; + + /** + * Command which is gonna be executed as part of fix. Used with ('powershell-command') + * @return {string} + */ + command; + + /** + * File which is gonna be executed as part of fix. Used with ('powershell-script') + * @return {string} + */ + file; +} \ No newline at end of file diff --git a/bin/environments/dependencies/docker-container/index.js b/bin/environments/dependencies/docker-container/index.js index d9174df..b738a4a 100644 --- a/bin/environments/dependencies/docker-container/index.js +++ b/bin/environments/dependencies/docker-container/index.js @@ -17,6 +17,19 @@ module.exports = new class { } } + /** + * Check if docker-container dependencies are available + * @returns {boolean} + */ + check() { + if (!docker.is_docker_running()) { + console.error(`Docker engine not running. Please start docker and retry command`); + return false; + } + + return true; + } + /** * Start docker container * @param container {Container} diff --git a/bin/environments/index.js b/bin/environments/index.js index d33290f..1a6050e 100644 --- a/bin/environments/index.js +++ b/bin/environments/index.js @@ -28,13 +28,13 @@ module.exports = new class { await this.#startOrStop(args); break; case args.config: - this.#showConfig(args.component); + this.#showConfig(args.keyword); break; case args.list: this.#listAllComponents(); break; case args.location: - this.#showLocation(args.component); + this.#showLocation(args.keyword); break; default: this.#showHelp(yargs); @@ -59,7 +59,7 @@ module.exports = new class { * @returns {Promise} */ async #startOrStop(args) { - const keyword = args.component != null ? args.component.toLowerCase() : null; + const keyword = args.keyword != null ? args.keyword.toLowerCase() : null; if (keyword == null) { console.error(`Must have a component keyword. Please attempt with ${chalk.blue('dever env [component]')}`); @@ -73,7 +73,7 @@ module.exports = new class { const component = components_handler.getComponent(keyword); if (component == null) { - console.log('Could not find component'); + console.error(chalk.redBright('Could not find component with keyword')); return; } @@ -131,13 +131,13 @@ module.exports = new class { */ #showLocation(keyword) { if (keyword == null) { - console.error(`Missing [component]. Please try again with ${chalk.green('dever env [component] --location')}`); + console.error(`Missing [keyword]. Please try again with ${chalk.green('dever env [keyword] --location')}`); return; } const component = components_handler.getComponent(keyword); if (component == null) { - console.error('Could not find component'); + console.error(chalk.redBright('Could not find component with keyword')); return; } @@ -158,7 +158,7 @@ module.exports = new class { } if (config == null) { - console.error(chalk.redBright(keyword == null ? 'Could not find dever configuration' : 'Could not find component')); + console.error(chalk.redBright(keyword == null ? 'Could not find dever configuration' : 'Could not find component with keyword')); return; } @@ -171,7 +171,7 @@ module.exports = new class { #listAllComponents() { const components = components_handler.getAllComponents(); if (components == null || components.length === 0) { - console.error(`could not find any components. Please try running ${chalk.green('dever init')}`); + console.error(`Could not find any components. Please try running ${chalk.green('dever init')}`); return; } @@ -198,7 +198,7 @@ module.exports = new class { */ #optionsWithoutComponent(yargs) { return yargs - .positional('component', { + .positional('keyword', { describe: 'Keyword for component', type: 'string' }) @@ -219,14 +219,9 @@ module.exports = new class { * @returns {object} */ #optionsWithComponent(yargs, keyword) { - const component = components_handler.getComponent(keyword); - if (component == null) { - return null; - } - const options = yargs - .positional('component', { - describe: 'Name of component to start or stop', + .positional('keyword', { + describe: 'Keyword for component', type: 'string' }) .option('start', { @@ -250,6 +245,11 @@ module.exports = new class { describe: 'Show component configuration' }); + const component = components_handler.getComponent(keyword); + if (component == null) { + return options; + } + const customOptions = this.#getCustomOptions(component.dependencies); return customOption.addOptionsToYargs(options, customOptions); } @@ -375,6 +375,7 @@ module.exports = new class { case "docker-compose": return docker_compose.check(); case "docker-container": + return docker_container.check(); case "run-command": case "powershell-script": case "powershell-command": @@ -416,7 +417,7 @@ class Args { * Component * @var {string} */ - component; + keyword; /** * How component location diff --git a/bin/fix/index.js b/bin/fix/index.js new file mode 100644 index 0000000..550e54a --- /dev/null +++ b/bin/fix/index.js @@ -0,0 +1,244 @@ +const readline = require("readline"); +const chalk = require('chalk'); + +const fix_config = require('../configuration/handleFixConfig'); +const powershell = require('../common/helper/powershell'); +const delayer = require("../common/helper/delayer"); + +module.exports = new class { + /** + * Handler for fixes + * @param yargs {object} + * @param args {FixArgs} + * @returns {Promise} + */ + async handler(yargs, args) { + switch (true) { + case args.list: + this.#showListOfProblems(); + break; + case args.show: + this.#showFix(args); + break; + default: + this.#showHelpOrFix(yargs, args); + } + } + + /** + * Generate default or component options + * @param yargs {object} + * @returns {*|Object} + */ + getOptions(yargs) { + const keyword = this.#getKeywordFromArgv(yargs.argv); + // Todo: How to handle .argv causing javascript execution not being able to continue when using --help + return keyword == null ? + this.#optionsWithoutComponent(yargs) : + this.#optionsWithComponent(yargs, keyword); + } + + /** + * Show help context menu when called + * @param yargs + * @return void + */ + #showHelp(yargs) { + yargs.showHelp(); + } + + /** + * Get all options without component + * @param yargs {object} + * @returns {object} + */ + #optionsWithoutComponent(yargs) { + return yargs + .positional('problem', { + describe: 'Name of problem that you would like to fix that is listed in dever.json', + type: 'string' + }) + .option('list', { + alias: 'l', + describe: 'List of all problems which is supported', + }); + } + + /** + * Get all component options + * @param yargs {object} + * @param keyword {string} + * @returns {object} + */ + #optionsWithComponent(yargs, keyword) { + return yargs + .option('show', { + alias: 's', + describe: `Shows what 'fix [problem]' will execute`, + }); + } + + /** + * Get keyword from yargs + * @param argv {object} + * @returns {null|*} + */ + #getKeywordFromArgv(argv) { + if (argv.length < 2) { + return null; + } + + return argv._[1]; + } + + /** + * Show in console what the 'fix [problem]' will execute + * @param args {FixArgs} + */ + #showFix(args) { + const fixes = fix_config.get(args.problem); + if (fixes == null) { + console.log(`fix could not be found`); + return; + } + + for (const fix of fixes) { + switch (fix.type) { + case "powershell-command": + case "powershell-script": + console.log(`${fix.type}: ${fix.command}`); + break; + default: + console.error('fix type not supported'); + } + } + } + + /** + * Show a list of problems which can be solved using 'fix [problem]' + */ + #showListOfProblems() { + const fixes = fix_config.getAll(); + if (fixes == null) { + console.log(`no 'fix' commands available in any dever.json`); + return; + } + + console.log(); + + for (const fix of fixes) { + console.log(chalk.blue(`'${fix.key}' from ${fix.component}`)); + console.log(chalk.green(`${fix.type}: ${fix.command}`)); + console.log(); + } + } + + /** + * Fix or show help depending on whether 'problem' is defined or not + * @param yargs {object} + * @param args {FixArgs} + */ + #showHelpOrFix(yargs, args) { + if (args.problem != null) { + this.#fix(args.problem).catch(console.error); + return; + } + + this.#showHelp(yargs); + } + + /** + * Fix problem + * @param problem {string} + */ + async #fix(problem) { + const fixes = fix_config.get(problem); + + const fix = await this.#getFix(fixes); + if (fix == null) { + console.error(`Fix not found!`); + return; + } + + switch (fix.type) { + case 'powershell-command': + powershell.executeSync(fix.command); + console.log(`fix: '${fix.key}' powershell-command has been executed.`); + break; + case 'powershell-script': + powershell.executeFileSync(fix.file); + console.log(`fix: '${fix.key}' powershell-script has been executed.`); + break; + default: + throw new Error('Fix type not supported'); + } + } + + /** + * Get fix that user chooses to run + * @param fixes {Fix[]} + * @returns {Promise} + */ + #getFix(fixes) { + if (fixes == null || fixes.length < 1) { + return null; + } + + if (fixes.length === 1) { + // Todo: Test if working + return new Promise((resolve) => resolve(fixes[0])); + } + + console.log(`Found multiple fixes with same keyword.`); + console.log('Please select one using the indicated number:'); + console.log(); + + let n = 0; + for (const fix of fixes) { + n++; + + console.log(chalk.blue(`${n}. '${fix.key}' from ${fix.component}`)); + console.log(chalk.green(`${fix.type}: ${fix.command}`)); + console.log(); + } + + const timer = delayer.create(); + + const rl = readline.createInterface(process.stdin, process.stdout); + rl.question('Choose which fix you would like to run [number]:', (answer) => { + let result = null; + + const option = +answer; + + if (typeof option === 'number' && option > 0 && option <= fixes.length) { + result = fixes[option - 1]; + } + + timer.done(result); + + rl.close(); + }); + + return timer.delay(36000000, null); + } +} + +class FixArgs { + /** + * Defined which 'problem' fix should solve + * @return {string} + */ + problem; + + /** + * Show command/file that will be executed when running 'fix [problem]' command + * @return {boolean} + */ + show; + + /** + * Show list of possible fixes + * @var {bool} + */ + list; +} \ No newline at end of file diff --git a/bin/index.js b/bin/index.js index 0501ade..256c77a 100644 --- a/bin/index.js +++ b/bin/index.js @@ -3,38 +3,38 @@ const yargs = require("yargs")(process.argv.slice(2)); const env = require('./environments'); -const install = require('./install'); const init = require('./init'); +const fix = require('./fix'); -const usage = "\nUsage: dever