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: support command-specific help and improve display quality #52

Merged
merged 10 commits into from
Aug 26, 2021
14 changes: 10 additions & 4 deletions src/CliModels.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Command = string;
export type Command = string | undefined;
export type HelpMessage = string;
export type Arguments = string[];

Expand All @@ -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 {
Expand All @@ -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);
}
}

8 changes: 8 additions & 0 deletions src/CommandsRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ 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<string, any> => ({
validate: <Validate options={cliInput.options} />,
context: contextRouter(cliInput)
});

export const commandsRouter = (cli: any): any => {
const helpMessage = new HelpMessageBuilder();
const cliInput = CliInput.createFromMeow(cli);
if (!cliInput.command) {
return <helpMessage.HelpComponent />;
}
if (cliInput.help) {
return <helpMessage.HelpComponent command={cliInput.command as CommandName} />;
}
return commandsDictionary(cliInput)[cliInput.command];
};
27 changes: 5 additions & 22 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <context-name> remove a context from the store
use <context-name> set any context as current
add <context-name> <filepath> 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',
Expand All @@ -43,6 +23,9 @@ const cli = meow(`
alias: 'f',
type: 'string',
isRequired: false
},
help: {
alias: 'h'
Souvikns marked this conversation as resolved.
Show resolved Hide resolved
}
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/components/Context/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React from 'react';

import { ListContexts,AddContext,RemoveContext,ShowCurrentContext,SetCurrent } from './Context';
Expand All @@ -13,5 +14,5 @@ const commandDictionary = (cliInput: CliInput): Record<string, any> => ({

export const contextRouter = (cliInput: CliInput): any => {
const subCommand = CliInput.createSubCommand(cliInput);
return commandDictionary(subCommand)[subCommand.command];
return commandDictionary(subCommand)[subCommand.command!];
};
30 changes: 0 additions & 30 deletions src/help-message.spec.ts

This file was deleted.

99 changes: 0 additions & 99 deletions src/help-message.ts

This file was deleted.

129 changes: 129 additions & 0 deletions src/help-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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,
'-v, --version output the version number',
];

readonly commands: Command = {
validate: {
usage: 'asyncapi validate [options]',
shortDescription: 'Validate asyncapi file',
flags: [
this.helpFlag,
'-f, --file <spec-file-path> Path of the AsyncAPI file',
'-c, --context <saved-context-name> 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 <context-name> set given context as default/current',
'add <context-name> <spec-file-path> add/update context',
'remove <context-name> remove a context'
]
}
}
}

export class HelpMessageBuilder {
private helpMessage: HelpMessage = container.resolve(HelpMessage);

HelpComponent: FunctionComponent<{ command?: CommandName }> = ({ command }) => {
if (command) {
if (!CommandList.includes(command)) {return <Text color="red">❌{' '} {command} is not a vaild command</Text>;}
const HelpComp = this.showCommandHelp;
return <HelpComp command={command} />;
}
const RootHelp = this.showHelp;
return <RootHelp />;
}

showHelp: FunctionComponent = () => {
return <>
<Text backgroundColor="greenBright" bold color="blackBright"> USAGE </Text>
<Newline />
<Text>
<Text color="greenBright">{this.helpMessage.usage.split(' ')[0]}</Text>{' '}
<Text color="yellowBright">{this.helpMessage.usage.split(' ')[1]}</Text>{' '}
<Text color="blueBright">{this.helpMessage.usage.split(' ')[2]}</Text>
</Text>
<Newline />

<Text backgroundColor="yellowBright" bold color="blackBright"> OPTIONS </Text>
<Newline />
{this.helpMessage.flags.map(flag => <Text key={flag}>
<Text color="yellowBright" bold>{flag.split(',')[0]}</Text>,{flag.split(',')[1]}
</Text>)}
<Newline />

<Text backgroundColor="blueBright" bold color="blackBright"> COMMANDS </Text>
<Newline />
{Object.keys(this.helpMessage.commands).map(command => <Text key={command}>
<Text color="blueBright" bold>{command}</Text>{' '} <Text>{this.helpMessage.commands[command as CommandName].shortDescription}</Text>
</Text>)}
</>;
}

showCommandHelp: FunctionComponent<{ command: CommandName }> = ({ command }) => {
const commandHelpObject = this.helpMessage.commands[command as CommandName];
return <>
<Text backgroundColor="greenBright" bold color="blackBright"> USAGE </Text>
<Newline />
<Text>
<Text color="greenBright">{commandHelpObject.usage.split(' ')[0]}</Text>{' '}
<Text>{commandHelpObject.usage.split(' ')[1]}</Text>{' '}
<Text color="yellowBright">{commandHelpObject.usage.split(' ')[2]}</Text>{' '}
<Text color="blueBright">{commandHelpObject.usage.split(' ')[3]}</Text>
</Text>
<Newline />

{commandHelpObject.longDescription ? <Text color="cyan">{commandHelpObject.longDescription}<Newline /></Text> : null}

<Text backgroundColor="yellowBright" bold color="blackBright"> OPTIONS </Text>
<Newline />
{commandHelpObject.flags.map(flag => <Text key={flag}><Text color="yellowBright">{flag.split(',')[0]}</Text>,{flag.split(',')[1]}</Text>)}
<Newline />

{commandHelpObject.subCommands ? <>
<Text backgroundColor="blueBright" bold color="blackBright"> COMMAND </Text>
<Newline />
{commandHelpObject.subCommands.map(cmd => {
const [cmdName, ...rest] = cmd.split(' ');
return <Text key={cmd}>
<Text color='blueBright'>{cmdName}</Text>{' '} {rest.map(el => <Text key={el}>{el} </Text>)}
</Text>;
})}
</> : null}

</>;
}
}