mirror of
https://github.com/chenasraf/massarg.git
synced 2026-05-18 01:39:05 +00:00
feat: improve help config, update styles, fixes
This commit is contained in:
@@ -31,13 +31,13 @@ export const StringStyle = z.object({
|
||||
bold: z.boolean().optional(),
|
||||
underline: z.boolean().optional(),
|
||||
color: zodEnumFromObjKeys(ansiColors).optional(),
|
||||
reset: z.boolean().optional(),
|
||||
reset: z.boolean().default(true).optional(),
|
||||
})
|
||||
|
||||
export type StringStyle = z.infer<typeof StringStyle>
|
||||
|
||||
export function format(string: string, style: StringStyle = {}): string {
|
||||
const { color, bold, underline, reset } = style
|
||||
const { color, bold, underline, reset = true } = style
|
||||
const colorCode = color ? ansiColors[color] : ''
|
||||
const boldCode = bold ? ansiStyles.bold : ''
|
||||
const underlineCode = underline ? ansiStyles.underline : ''
|
||||
@@ -46,5 +46,5 @@ export function format(string: string, style: StringStyle = {}): string {
|
||||
}
|
||||
|
||||
export function stripColors(string: string): string {
|
||||
return string.replace(/\x1b\[\d+m/g, '')
|
||||
return string.replace(/\x1b\[\d+m/gi, '')
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import MassargOption, {
|
||||
TypedOptionConfig,
|
||||
MassargHelpFlag,
|
||||
} from './option'
|
||||
import { setOrPush, deepMerge } from './utils'
|
||||
import { DeepRequired, setOrPush, deepMerge } from './utils'
|
||||
import MassargExample, { ExampleConfig } from './example'
|
||||
|
||||
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
@@ -48,15 +48,24 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
options: MassargOption[] = []
|
||||
examples: MassargExample[] = []
|
||||
args: Partial<Args> = {}
|
||||
helpConfig: Required<HelpConfig>
|
||||
private _helpConfig: HelpConfig
|
||||
parent?: MassargCommand<any>
|
||||
|
||||
constructor(options: CommandConfig<Args>) {
|
||||
constructor(options: CommandConfig<Args>, parent?: MassargCommand<any>) {
|
||||
CommandConfig(z.any()).parse(options)
|
||||
this.name = options.name
|
||||
this.description = options.description
|
||||
this.aliases = options.aliases ?? []
|
||||
this._run = options.run
|
||||
this.helpConfig = HelpConfig.required().parse(defaultHelpConfig)
|
||||
this._helpConfig = HelpConfig.required().parse(defaultHelpConfig)
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
get helpConfig(): DeepRequired<HelpConfig> {
|
||||
if (this.parent) {
|
||||
return deepMerge(this.parent.helpConfig, this._helpConfig)
|
||||
}
|
||||
return deepMerge(defaultHelpConfig, this._helpConfig) as Required<HelpConfig>
|
||||
}
|
||||
|
||||
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
|
||||
@@ -74,6 +83,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
path: [this.name, command.name],
|
||||
})
|
||||
}
|
||||
command.parent = this
|
||||
this.commands.push(command)
|
||||
return this
|
||||
} catch (e) {
|
||||
@@ -161,9 +171,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
|
||||
help(config: HelpConfig): MassargCommand<Args> {
|
||||
this.helpConfig = HelpConfig.required().parse(
|
||||
deepMerge(defaultHelpConfig, config) as HelpConfig,
|
||||
)
|
||||
this._helpConfig = HelpConfig.parse(config)
|
||||
|
||||
if (this.helpConfig.bindCommand) {
|
||||
this.command(new MassargHelpCommand())
|
||||
|
||||
101
src/help.ts
101
src/help.ts
@@ -1,7 +1,7 @@
|
||||
import z from 'zod'
|
||||
import { format, StringStyle, stripColors } from './color'
|
||||
import MassargCommand from './command'
|
||||
import { chainStr, indent } from './utils'
|
||||
import { DeepRequired, strConcat, indent } from './utils'
|
||||
|
||||
export const GenerateTableCommandConfig = z.object({
|
||||
maxRowLength: z.number().optional(),
|
||||
@@ -44,11 +44,14 @@ export const HelpConfig = z.object({
|
||||
output: StringStyle.optional(),
|
||||
})
|
||||
.optional(),
|
||||
usageText: z.string().optional(),
|
||||
headerText: z.string().optional(),
|
||||
footerText: z.string().optional(),
|
||||
})
|
||||
|
||||
export type HelpConfig = z.infer<typeof HelpConfig>
|
||||
|
||||
export const defaultHelpConfig: HelpConfig = {
|
||||
export const defaultHelpConfig: DeepRequired<HelpConfig> = {
|
||||
maxRowLength: 80,
|
||||
commandOptions: {
|
||||
namePrefix: '',
|
||||
@@ -97,6 +100,9 @@ export const defaultHelpConfig: HelpConfig = {
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
},
|
||||
headerText: '',
|
||||
footerText: '',
|
||||
usageText: '',
|
||||
}
|
||||
|
||||
export type HelpItem = {
|
||||
@@ -107,15 +113,18 @@ export type HelpItem = {
|
||||
|
||||
export class HelpGenerator {
|
||||
entry: MassargCommand<any>
|
||||
config: HelpConfig
|
||||
config: DeepRequired<HelpConfig>
|
||||
|
||||
constructor(entry: MassargCommand<any>, config?: HelpConfig) {
|
||||
this.entry = entry
|
||||
this.config = HelpConfig.parse({
|
||||
this.config = HelpConfig.required().parse({
|
||||
...entry.helpConfig,
|
||||
commandOptions: {
|
||||
...entry.helpConfig?.commandOptions,
|
||||
...config?.commandOptions,
|
||||
},
|
||||
optionOptions: {
|
||||
...entry.helpConfig?.optionOptions,
|
||||
...config?.optionOptions,
|
||||
},
|
||||
})
|
||||
@@ -123,40 +132,53 @@ export class HelpGenerator {
|
||||
|
||||
generate(): string {
|
||||
const entry = this.entry
|
||||
const options = generateHelpTable(entry.options, this.config.optionOptions)
|
||||
const commands = generateHelpTable(entry.commands, this.config.commandOptions)
|
||||
const CMD_OPT_INDENT = 4
|
||||
const _wrap = (text: string, indent = 0) => wrap(text, this.config.maxRowLength - indent)
|
||||
const options = generateHelpTable(entry.options, {
|
||||
...this.config.optionOptions,
|
||||
maxRowLength:
|
||||
(this.config.optionOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
|
||||
})
|
||||
const commands = generateHelpTable(entry.commands, {
|
||||
...this.config.commandOptions,
|
||||
maxRowLength:
|
||||
(this.config.commandOptions.maxRowLength ?? this.config.maxRowLength) - CMD_OPT_INDENT,
|
||||
})
|
||||
const examples = entry.examples
|
||||
.map((example) => {
|
||||
const { description, input, output } = example
|
||||
return chainStr(
|
||||
description && [format(description, this.config.exampleStyles?.description), ''],
|
||||
input && format(input, this.config.exampleStyles?.input),
|
||||
output && format(output, this.config.exampleStyles?.output),
|
||||
return strConcat(
|
||||
description && [_wrap(format(description, this.config.exampleStyles.description), 4), ''],
|
||||
input && _wrap(format('$ ' + input, this.config.exampleStyles.input), 4),
|
||||
output && _wrap(format('> ' + output, this.config.exampleStyles.output), 4),
|
||||
)
|
||||
})
|
||||
.join('\n')
|
||||
const { headerText, footerText, usageText } = this.config
|
||||
|
||||
return (
|
||||
chainStr(
|
||||
format(`Usage: ${entry.name} [...options]`, this.config.usageStyle),
|
||||
strConcat(
|
||||
_wrap(format(usageText || `Usage: ${entry.name} [...options]`, this.config.usageStyle)),
|
||||
headerText.length && ['', format(headerText, this.config.descriptionStyle)],
|
||||
'',
|
||||
format(entry.description, this.config.descriptionStyle),
|
||||
_wrap(format(entry.description, this.config.descriptionStyle)),
|
||||
commands.length &&
|
||||
indent([
|
||||
'',
|
||||
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
|
||||
'',
|
||||
indent(commands),
|
||||
]),
|
||||
indent([
|
||||
'',
|
||||
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
|
||||
'',
|
||||
indent(commands),
|
||||
]),
|
||||
options.length &&
|
||||
indent([
|
||||
'',
|
||||
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
|
||||
'',
|
||||
indent(options),
|
||||
]),
|
||||
indent([
|
||||
'',
|
||||
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
|
||||
'',
|
||||
indent(options),
|
||||
]),
|
||||
examples.length &&
|
||||
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
|
||||
indent(['', format('Examples:', this.config.subtitleStyle), '', indent(examples)]),
|
||||
footerText.length && ['', _wrap(format(footerText, this.config.descriptionStyle))],
|
||||
) + '\n'
|
||||
)
|
||||
}
|
||||
@@ -166,6 +188,28 @@ export class HelpGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(text: string, maxRowLength: number): string {
|
||||
const length = stripColors(text).length
|
||||
if (length <= maxRowLength) {
|
||||
return text
|
||||
}
|
||||
const subRows: string[] = []
|
||||
const words = text.split(' ')
|
||||
let currentRow = ''
|
||||
console.log('words', words)
|
||||
|
||||
for (const word of words) {
|
||||
if (stripColors(currentRow).length + stripColors(word).length + 1 > maxRowLength) {
|
||||
subRows.push(currentRow)
|
||||
currentRow = ''
|
||||
}
|
||||
currentRow += `${word} `
|
||||
}
|
||||
subRows.push(currentRow)
|
||||
|
||||
return subRows.join('\n')
|
||||
}
|
||||
|
||||
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||
items: HelpItem[],
|
||||
{
|
||||
@@ -177,8 +221,9 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||
}: Partial<T> = {},
|
||||
): string {
|
||||
const rows = items.map((o) => {
|
||||
const name = `${namePrefix}${o.name}${o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
|
||||
}`
|
||||
const name = `${namePrefix}${o.name}${
|
||||
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ''
|
||||
}`
|
||||
const description = o.description
|
||||
return { name, description }
|
||||
})
|
||||
|
||||
@@ -75,9 +75,13 @@ const removeCmd = new MassargCommand<{ component: string }>({
|
||||
const main = massarg<A>({
|
||||
name: 'my-cli',
|
||||
description: 'This is an example CLI',
|
||||
bindHelpOption: true,
|
||||
bindHelpCommand: true,
|
||||
})
|
||||
.help({
|
||||
bindOption: true,
|
||||
bindCommand: true,
|
||||
headerText: 'This is a header',
|
||||
footerText: 'This is a footer',
|
||||
})
|
||||
.main((opts, parser) => {
|
||||
console.log('Main command - printing all opts')
|
||||
console.log(opts, '\n')
|
||||
@@ -97,6 +101,17 @@ const main = massarg<A>({
|
||||
aliases: ['n'],
|
||||
type: 'number',
|
||||
})
|
||||
.example({
|
||||
description: 'Example main command',
|
||||
input: 'my-cli --bool --number 123',
|
||||
output: 'Main command - printing all opts\n{ bool: true, number: 123 }\n',
|
||||
})
|
||||
.example({
|
||||
description: 'Example add command',
|
||||
input: 'my-cli add --component foo --classes bar --classes baz --custom 123',
|
||||
output:
|
||||
'Duis ad consectetur dolore elit laborum do et aute consequat magna eu consequat dolore dolor commodo sit enim reprehenderit lorem consectetur adipiscing officia nisi adipiscing consequat consequat labore sint incididunt',
|
||||
})
|
||||
|
||||
// console.log("Opts:", main.getArgs(process.argv.slice(2)), "\n")
|
||||
|
||||
|
||||
10
src/utils.ts
10
src/utils.ts
@@ -12,7 +12,11 @@ export function setOrPush<T>(
|
||||
}
|
||||
type Parseable = string | number | boolean | null | undefined | Record<string, unknown>
|
||||
|
||||
export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
||||
export type DeepRequired<T> = {
|
||||
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : NonNullable<T[P]>
|
||||
}
|
||||
|
||||
export function strConcat(...strs: (Parseable | Parseable[])[]) {
|
||||
const res: string[] = []
|
||||
for (const str of strs) {
|
||||
if (typeof str === 'string') {
|
||||
@@ -20,7 +24,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(str)) {
|
||||
res.push(chainStr(...str))
|
||||
res.push(strConcat(...str))
|
||||
continue
|
||||
}
|
||||
if (str == null) {
|
||||
@@ -43,7 +47,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
||||
}
|
||||
|
||||
export function indent(str: Parseable | Parseable[], indent = 2): string {
|
||||
return chainStr(str)
|
||||
return strConcat(str)
|
||||
.split('\n')
|
||||
.map((s) => ' '.repeat(indent) + s)
|
||||
.join('\n')
|
||||
|
||||
Reference in New Issue
Block a user