diff --git a/README.md b/README.md index 6712d25581d..2985f16b901 100644 --- a/README.md +++ b/README.md @@ -13,31 +13,66 @@ $ npm install --global @asyncapi/cli ``` +### Getting Started +Go ahead and run command `asyncapi --help` to get complete help for using CLI. If having doubt about any particular command do run `asyncapi --help` to get help for that command. + ### CLI +Help string for all the supported commands + +- #### `asyncapi --help` +``` +USAGE + +asyncapi [options] [command] + +OPTIONS + +-h, --help display help for command +--version output the version number + +COMMANDS + +validate validate asyncapi file +context Manage contexts +``` + +- #### `asyncapi validate --help` ``` -$ asyncapi --help - -Usage - $ asyncapi command options - -Commands - validate - Options - -c --context context-name saved in the store - -w --watch Enable watchMode (not implemented yet) - -f --file File path of the specification file - - context - current show the current set context - list show the list of all stored contexts - remove remove a context from the store - use set any context from store as current - add add/update new context - -Examples - $ asyncapi context add dummy ./asyncapi.yml - $ asyncapi validate --context=dummy - $ asyncapi validate --file=./asyncapi.yml +USAGE + +asyncapi validate [options] + +OPTIONS + +-h, --help display help for command +-f, --file Path of the asyncapi file +-c, --context context name to use +-w, --watch Enable Watch Mode (not implemented yet) +``` + +- #### `asyncapi context --help` +``` +USAGE + +asyncapi context [options] [command] + +Context is what makes it easier for you to work with multiple AsyncAPI files. +You can add multiple different files to a contextThis way you do not have to pass +--file flag with path to the file every time but just --context flag with reference name +You can also set a default context, so neither --file nor --context flags are needed. + +OPTIONS + +-h, --help display help for command + +COMMANDS + +list list of all saved contexts +current see current context +use set given context as default/current +add add/update context +remove remove a context + ``` > For now --context flag is requried to run validate command \ No newline at end of file diff --git a/src/CliModels.ts b/src/CliModels.ts index 4f4ff68ae93..79fa0e38caf 100644 --- a/src/CliModels.ts +++ b/src/CliModels.ts @@ -1,4 +1,4 @@ -export type Command = string; +export type Command = string | undefined; export type HelpMessage = string; export type Arguments = string[]; @@ -12,11 +12,13 @@ export class CliInput { private readonly _command: Command private readonly _options: Options private readonly _arguments: Arguments + private readonly _help: boolean | undefined - private constructor(command: Command, options: Options, args: Arguments) { + private constructor(command: Command, options: Options, args: Arguments, help?:boolean) { this._command = command; this._options = options; this._arguments = args; + this._help = help; } get command(): Command { @@ -31,15 +33,19 @@ export class CliInput { return this._arguments; } + get help(): boolean | undefined { + return this._help; + } + static createFromMeow(meowOutput: any): CliInput { const [command, ...args] = meowOutput.input; const { context, watch, file } = meowOutput.flags; - return new CliInput(command || 'help', { context, watch, file }, args); + return new CliInput(command, { context, watch, file }, args, meowOutput.flags.help); } static createSubCommand(cliInput: CliInput): CliInput { const [command, ...args] = cliInput.arguments; - return new CliInput(command || 'help', cliInput.options, args); + return new CliInput(command, cliInput.options, args); } } diff --git a/src/CommandsRouter.tsx b/src/CommandsRouter.tsx index f853f25839e..d6bc6ea9899 100644 --- a/src/CommandsRouter.tsx +++ b/src/CommandsRouter.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Validate from './components/Validate/Validate'; import { contextRouter } from './components/Context'; import { CliInput } from './CliModels'; +import { CommandName, HelpMessageBuilder } from './help-message'; const commandsDictionary = (cliInput: CliInput): Record => ({ validate: , @@ -9,6 +10,13 @@ const commandsDictionary = (cliInput: CliInput): Record => ({ }); export const commandsRouter = (cli: any): any => { + const helpMessage = new HelpMessageBuilder(); const cliInput = CliInput.createFromMeow(cli); + if (!cliInput.command) { + return ; + } + if (cliInput.help) { + return ; + } return commandsDictionary(cliInput)[cliInput.command]; }; diff --git a/src/cli.ts b/src/cli.ts index e037df0355e..8f18a85a467 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,28 +5,8 @@ import { render } from 'ink'; import meow from 'meow'; import { commandsRouter } from './CommandsRouter'; -const cli = meow(` - Usage - $ asyncapi command options - - Commands - validate - Options - -c --context context-name saved in the store - -w --watch Enable watchMode (not implemented yet) - -f --file File path of the specification file - context - current show the current set context - list show the list of all stored context - remove remove a context from the store - use set any context as current - add add/update new context - - Examples - $ asyncapi context add dummy ./asyncapi.yml - $ asyncapi validate --context=dummy - $ asyncapi validate --file=./asyncapi.yml -`, { +const cli = meow({ + autoHelp: false, flags: { context: { alias: 'c', @@ -43,6 +23,9 @@ const cli = meow(` alias: 'f', type: 'string', isRequired: false + }, + help: { + alias: 'h' } } }); diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx index cde7ca1e500..b45c9d19562 100644 --- a/src/components/Context/index.tsx +++ b/src/components/Context/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import React from 'react'; import { ListContexts,AddContext,RemoveContext,ShowCurrentContext,SetCurrent } from './Context'; @@ -13,5 +14,5 @@ const commandDictionary = (cliInput: CliInput): Record => ({ export const contextRouter = (cliInput: CliInput): any => { const subCommand = CliInput.createSubCommand(cliInput); - return commandDictionary(subCommand)[subCommand.command]; + return commandDictionary(subCommand)[subCommand.command!]; }; diff --git a/src/help-message.spec.ts b/src/help-message.spec.ts deleted file mode 100644 index 76ccf227bf4..00000000000 --- a/src/help-message.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { HelpMessageBuilder } from './help-message'; - -let helpBuilder: HelpMessageBuilder; - -describe('HelpMessageBuilder should', () => { - beforeAll(() => { - helpBuilder = new HelpMessageBuilder(); - }); - it('return root Help message', () => { - expect(typeof helpBuilder.showHelp()).toMatch('string'); - expect(helpBuilder.showHelp()).toMatch( - 'usage: asyncapi [options] [command]\n\n'+ - 'flags:\n'+ - ' -h, --help display help for command\n'+ - ' -v, --version output the version number\n'+ - '\n'+ - 'commands:\n'+ - ' validate [options] [command] Validate asyncapi file\n'+ - ' context [options] [command] Manage context\n' - ); - }); - - it('return validate help message', () => { - expect(typeof helpBuilder.showCommandHelp('validate')).toMatch('string'); - }); - - it('return context help message', () => { - expect(typeof helpBuilder.showCommandHelp('context')).toMatch('string'); - }); -}); diff --git a/src/help-message.ts b/src/help-message.ts deleted file mode 100644 index e620d05b66f..00000000000 --- a/src/help-message.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { injectable, container } from 'tsyringe'; - -export type CommandName = 'validate' | 'context'; - -export type Command = { - [name in CommandName]: { - usage: string; - shortDescription: string; - longDescription?: string; - flags: string[]; - subCommands?: string[]; - }; -}; - -@injectable() -export class HelpMessage { - private helpFlag = '-h, --help display help for command'; - - readonly usage: string = 'asyncapi [options] [command]'; - - readonly flags = [ - this.helpFlag, - '-v, --version output the version number', - ]; - - readonly commands: Command = { - validate: { - usage: 'asyncapi validate [options]', - shortDescription: 'Validate asyncapi file', - flags: [ - this.helpFlag, - '-f, --file Path of the AsyncAPI file', - '-c, --context Context to use', - '-w, --watch Watch mode' - ] - }, - context: { - usage: 'asyncapi context [command] [options]', - shortDescription: 'Manage context', - longDescription: 'Context is what makes it easier for you to work with multiple AsyncAPI files.\nYou can add multiple different files to a context.\nThis way you do not have to pass --file flag with path to the file every time but just --context flag with reference name.\nYou can also set a default context, so neither --file nor --context flags are needed', - flags: [this.helpFlag], - subCommands: [ - 'list list all saved contexts', - 'current see current context', - 'use set given context as default/current', - 'add add/update context', - 'remove remove a context' - ] - } - } -} - -export class HelpMessageBuilder { - private helpMessage: HelpMessage = container.resolve(HelpMessage); - - showHelp() { - let helpText = ''; - helpText += `usage: ${this.helpMessage.usage}\n\n`; - helpText += 'flags:\n'; - for (const flag of this.helpMessage.flags) { - helpText += ` ${flag}\n`; - } - helpText += '\n'; - - if (this.helpMessage.commands) { - helpText += 'commands:\n'; - for (const [name, obj] of Object.entries(this.helpMessage.commands)) { - helpText += ` ${name} [options] [command] ${obj.shortDescription}\n`; - } - } - - return helpText; - } - - showCommandHelp(command: CommandName) { - let helpText = ''; - const commandHelpObject = this.helpMessage.commands[command as CommandName]; - helpText += `usage: ${commandHelpObject.usage}\n\n`; - - if (commandHelpObject.longDescription) { - helpText += `${commandHelpObject.longDescription}\n\n`; - } - - helpText += 'flags: \n'; - for (const flag of commandHelpObject.flags) { - helpText += ` ${flag}\n`; - } - - if (commandHelpObject.subCommands) { - helpText += '\n'; - helpText += 'commands:\n'; - for (const command of commandHelpObject.subCommands) { - helpText += ` ${command}\n`; - } - } - - return helpText; - } -} diff --git a/src/help-message.tsx b/src/help-message.tsx new file mode 100644 index 00000000000..402fb397f2d --- /dev/null +++ b/src/help-message.tsx @@ -0,0 +1,145 @@ +import { injectable, container } from 'tsyringe'; +import React, { FunctionComponent } from 'react'; +import { Text, Newline } from 'ink'; +const CommandList = ['validate', 'context'] as const; + +export type CommandName = typeof CommandList[number] + +export type Command = { + [name in CommandName]: { + usage: string; + shortDescription: string; + longDescription?: string; + flags: string[]; + subCommands?: string[]; + }; +}; + +@injectable() +export class HelpMessage { + private helpFlag = '-h, --help display help for command'; + + readonly usage: string = 'asyncapi [options] [command]'; + + readonly flags = [ + this.helpFlag, + '--version, output version string' + ]; + + readonly commands: Command = { + validate: { + usage: 'asyncapi validate [options]', + shortDescription: 'Validate asyncapi file', + flags: [ + this.helpFlag, + '-f, --file Path of the AsyncAPI file', + '-c, --context Context to use', + '-w, --watch Enable watch mode (not implemented yet)' + ] + }, + context: { + usage: 'asyncapi context [options] [command]', + shortDescription: 'Manage context', + longDescription: 'Context is what makes it easier for you to work with multiple AsyncAPI files.\nYou can add multiple different files to a context.\nThis way you do not have to pass --file flag with path to the file every time but just --context flag with reference name.\nYou can also set a default context, so neither --file nor --context flags are needed', + flags: [this.helpFlag], + subCommands: [ + 'list list all saved contexts', + 'current see current context', + 'use set given context as default/current', + 'add add/update context', + 'remove remove a context' + ] + } + } +} + +export class HelpMessageBuilder { + private helpMessage: HelpMessage = container.resolve(HelpMessage); + + HelpComponent: FunctionComponent<{ command?: CommandName }> = ({ command }) => { + if (command) { + if (!CommandList.includes(command)) { return ❌{' '} {command} is not a vaild command; } + const HelpComp = this.showCommandHelp; + return ; + } + const RootHelp = this.showHelp; + return ; + } + + showHelp: FunctionComponent = () => { + return <> + USAGE + + + {this.helpMessage.usage.split(' ')[0]}{' '} + {this.helpMessage.usage.split(' ')[1]}{' '} + {this.helpMessage.usage.split(' ')[2]} + + + + OPTIONS + + {this.helpMessage.flags.map(flag => { + const [alias, ...rest] = flag.split(','); + const flg = rest[0]?.split(' ')[1]; + const msg = rest[0]?.split(' ').splice(2, rest[0].length).join(' '); + if (alias === '--version') { + return + {alias} {rest} + ; + } + return + {alias},{flg} {msg} + ; + })} + + + COMMANDS + + {Object.keys(this.helpMessage.commands).map(command => + {command}{' '} {this.helpMessage.commands[command as CommandName].shortDescription} + )} + ; + } + + showCommandHelp: FunctionComponent<{ command: CommandName }> = ({ command }) => { + const commandHelpObject = this.helpMessage.commands[command as CommandName]; + return <> + USAGE + + + {commandHelpObject.usage.split(' ')[0]}{' '} + {commandHelpObject.usage.split(' ')[1]}{' '} + {commandHelpObject.usage.split(' ')[2]}{' '} + {commandHelpObject.usage.split(' ')[3]} + + + + {commandHelpObject.longDescription ? {commandHelpObject.longDescription} : null} + + OPTIONS + + {commandHelpObject.flags.map(flag => { + const [alias, ...rest] = flag.split(','); + const flg = rest[0]?.split(' ')[1]; + const msg = rest[0]?.split(' ').splice(2, rest[0].length).join(' '); + return + {alias}, {flg} {msg} + ; + })} + + + {commandHelpObject.subCommands ? <> + COMMAND + + {commandHelpObject.subCommands.map(cmd => { + const [cmdName, ...rest] = cmd.split(' '); + return + {cmdName}{' '} {rest.map(el => {el} )} + ; + })} + : null} + + ; + } +}