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(),
|
bold: z.boolean().optional(),
|
||||||
underline: z.boolean().optional(),
|
underline: z.boolean().optional(),
|
||||||
color: zodEnumFromObjKeys(ansiColors).optional(),
|
color: zodEnumFromObjKeys(ansiColors).optional(),
|
||||||
reset: z.boolean().optional(),
|
reset: z.boolean().default(true).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type StringStyle = z.infer<typeof StringStyle>
|
export type StringStyle = z.infer<typeof StringStyle>
|
||||||
|
|
||||||
export function format(string: string, style: StringStyle = {}): string {
|
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 colorCode = color ? ansiColors[color] : ''
|
||||||
const boldCode = bold ? ansiStyles.bold : ''
|
const boldCode = bold ? ansiStyles.bold : ''
|
||||||
const underlineCode = underline ? ansiStyles.underline : ''
|
const underlineCode = underline ? ansiStyles.underline : ''
|
||||||
@@ -46,5 +46,5 @@ export function format(string: string, style: StringStyle = {}): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stripColors(string: string): 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,
|
TypedOptionConfig,
|
||||||
MassargHelpFlag,
|
MassargHelpFlag,
|
||||||
} from './option'
|
} from './option'
|
||||||
import { setOrPush, deepMerge } from './utils'
|
import { DeepRequired, setOrPush, deepMerge } from './utils'
|
||||||
import MassargExample, { ExampleConfig } from './example'
|
import MassargExample, { ExampleConfig } from './example'
|
||||||
|
|
||||||
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||||
@@ -48,15 +48,24 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
|||||||
options: MassargOption[] = []
|
options: MassargOption[] = []
|
||||||
examples: MassargExample[] = []
|
examples: MassargExample[] = []
|
||||||
args: Partial<Args> = {}
|
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)
|
CommandConfig(z.any()).parse(options)
|
||||||
this.name = options.name
|
this.name = options.name
|
||||||
this.description = options.description
|
this.description = options.description
|
||||||
this.aliases = options.aliases ?? []
|
this.aliases = options.aliases ?? []
|
||||||
this._run = options.run
|
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>
|
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],
|
path: [this.name, command.name],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
command.parent = this
|
||||||
this.commands.push(command)
|
this.commands.push(command)
|
||||||
return this
|
return this
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -161,9 +171,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
help(config: HelpConfig): MassargCommand<Args> {
|
help(config: HelpConfig): MassargCommand<Args> {
|
||||||
this.helpConfig = HelpConfig.required().parse(
|
this._helpConfig = HelpConfig.parse(config)
|
||||||
deepMerge(defaultHelpConfig, config) as HelpConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (this.helpConfig.bindCommand) {
|
if (this.helpConfig.bindCommand) {
|
||||||
this.command(new MassargHelpCommand())
|
this.command(new MassargHelpCommand())
|
||||||
|
|||||||
101
src/help.ts
101
src/help.ts
@@ -1,7 +1,7 @@
|
|||||||
import z from 'zod'
|
import z from 'zod'
|
||||||
import { format, StringStyle, stripColors } from './color'
|
import { format, StringStyle, stripColors } from './color'
|
||||||
import MassargCommand from './command'
|
import MassargCommand from './command'
|
||||||
import { chainStr, indent } from './utils'
|
import { DeepRequired, strConcat, indent } from './utils'
|
||||||
|
|
||||||
export const GenerateTableCommandConfig = z.object({
|
export const GenerateTableCommandConfig = z.object({
|
||||||
maxRowLength: z.number().optional(),
|
maxRowLength: z.number().optional(),
|
||||||
@@ -44,11 +44,14 @@ export const HelpConfig = z.object({
|
|||||||
output: StringStyle.optional(),
|
output: StringStyle.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
usageText: z.string().optional(),
|
||||||
|
headerText: z.string().optional(),
|
||||||
|
footerText: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type HelpConfig = z.infer<typeof HelpConfig>
|
export type HelpConfig = z.infer<typeof HelpConfig>
|
||||||
|
|
||||||
export const defaultHelpConfig: HelpConfig = {
|
export const defaultHelpConfig: DeepRequired<HelpConfig> = {
|
||||||
maxRowLength: 80,
|
maxRowLength: 80,
|
||||||
commandOptions: {
|
commandOptions: {
|
||||||
namePrefix: '',
|
namePrefix: '',
|
||||||
@@ -97,6 +100,9 @@ export const defaultHelpConfig: HelpConfig = {
|
|||||||
color: 'brightWhite',
|
color: 'brightWhite',
|
||||||
underline: true,
|
underline: true,
|
||||||
},
|
},
|
||||||
|
headerText: '',
|
||||||
|
footerText: '',
|
||||||
|
usageText: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HelpItem = {
|
export type HelpItem = {
|
||||||
@@ -107,15 +113,18 @@ export type HelpItem = {
|
|||||||
|
|
||||||
export class HelpGenerator {
|
export class HelpGenerator {
|
||||||
entry: MassargCommand<any>
|
entry: MassargCommand<any>
|
||||||
config: HelpConfig
|
config: DeepRequired<HelpConfig>
|
||||||
|
|
||||||
constructor(entry: MassargCommand<any>, config?: HelpConfig) {
|
constructor(entry: MassargCommand<any>, config?: HelpConfig) {
|
||||||
this.entry = entry
|
this.entry = entry
|
||||||
this.config = HelpConfig.parse({
|
this.config = HelpConfig.required().parse({
|
||||||
|
...entry.helpConfig,
|
||||||
commandOptions: {
|
commandOptions: {
|
||||||
|
...entry.helpConfig?.commandOptions,
|
||||||
...config?.commandOptions,
|
...config?.commandOptions,
|
||||||
},
|
},
|
||||||
optionOptions: {
|
optionOptions: {
|
||||||
|
...entry.helpConfig?.optionOptions,
|
||||||
...config?.optionOptions,
|
...config?.optionOptions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -123,40 +132,53 @@ export class HelpGenerator {
|
|||||||
|
|
||||||
generate(): string {
|
generate(): string {
|
||||||
const entry = this.entry
|
const entry = this.entry
|
||||||
const options = generateHelpTable(entry.options, this.config.optionOptions)
|
const CMD_OPT_INDENT = 4
|
||||||
const commands = generateHelpTable(entry.commands, this.config.commandOptions)
|
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
|
const examples = entry.examples
|
||||||
.map((example) => {
|
.map((example) => {
|
||||||
const { description, input, output } = example
|
const { description, input, output } = example
|
||||||
return chainStr(
|
return strConcat(
|
||||||
description && [format(description, this.config.exampleStyles?.description), ''],
|
description && [_wrap(format(description, this.config.exampleStyles.description), 4), ''],
|
||||||
input && format(input, this.config.exampleStyles?.input),
|
input && _wrap(format('$ ' + input, this.config.exampleStyles.input), 4),
|
||||||
output && format(output, this.config.exampleStyles?.output),
|
output && _wrap(format('> ' + output, this.config.exampleStyles.output), 4),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
const { headerText, footerText, usageText } = this.config
|
||||||
|
|
||||||
return (
|
return (
|
||||||
chainStr(
|
strConcat(
|
||||||
format(`Usage: ${entry.name} [...options]`, this.config.usageStyle),
|
_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 &&
|
commands.length &&
|
||||||
indent([
|
indent([
|
||||||
'',
|
'',
|
||||||
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
|
format(`Commands for ${entry.name}:`, this.config.subtitleStyle),
|
||||||
'',
|
'',
|
||||||
indent(commands),
|
indent(commands),
|
||||||
]),
|
]),
|
||||||
options.length &&
|
options.length &&
|
||||||
indent([
|
indent([
|
||||||
'',
|
'',
|
||||||
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
|
format(`Options for ${entry.name}:`, this.config.subtitleStyle),
|
||||||
'',
|
'',
|
||||||
indent(options),
|
indent(options),
|
||||||
]),
|
]),
|
||||||
examples.length &&
|
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'
|
) + '\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>>(
|
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||||
items: HelpItem[],
|
items: HelpItem[],
|
||||||
{
|
{
|
||||||
@@ -177,8 +221,9 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
|||||||
}: Partial<T> = {},
|
}: Partial<T> = {},
|
||||||
): string {
|
): string {
|
||||||
const rows = items.map((o) => {
|
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
|
const description = o.description
|
||||||
return { name, description }
|
return { name, description }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,9 +75,13 @@ const removeCmd = new MassargCommand<{ component: string }>({
|
|||||||
const main = massarg<A>({
|
const main = massarg<A>({
|
||||||
name: 'my-cli',
|
name: 'my-cli',
|
||||||
description: 'This is an example 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) => {
|
.main((opts, parser) => {
|
||||||
console.log('Main command - printing all opts')
|
console.log('Main command - printing all opts')
|
||||||
console.log(opts, '\n')
|
console.log(opts, '\n')
|
||||||
@@ -97,6 +101,17 @@ const main = massarg<A>({
|
|||||||
aliases: ['n'],
|
aliases: ['n'],
|
||||||
type: 'number',
|
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")
|
// 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>
|
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[] = []
|
const res: string[] = []
|
||||||
for (const str of strs) {
|
for (const str of strs) {
|
||||||
if (typeof str === 'string') {
|
if (typeof str === 'string') {
|
||||||
@@ -20,7 +24,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (Array.isArray(str)) {
|
if (Array.isArray(str)) {
|
||||||
res.push(chainStr(...str))
|
res.push(strConcat(...str))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (str == null) {
|
if (str == null) {
|
||||||
@@ -43,7 +47,7 @@ export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function indent(str: Parseable | Parseable[], indent = 2): string {
|
export function indent(str: Parseable | Parseable[], indent = 2): string {
|
||||||
return chainStr(str)
|
return strConcat(str)
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((s) => ' '.repeat(indent) + s)
|
.map((s) => ' '.repeat(indent) + s)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|||||||
Reference in New Issue
Block a user