mirror of
https://github.com/chenasraf/massarg.git
synced 2026-05-18 01:39:05 +00:00
feat: example lines, help style updates
This commit is contained in:
@@ -17,9 +17,9 @@
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json && cp package.json README.md build",
|
||||
"dev": "tsc --watch",
|
||||
"example": "ts-node src/example.ts",
|
||||
"cmd": "ts-node src/sample.ts",
|
||||
"test": "jest",
|
||||
"docs": "typedoc --out docs src --plugin typedoc-plugin-zod --theme default"
|
||||
"docgen": "typedoc --out docs src --plugin typedoc-plugin-zod --theme default"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.8",
|
||||
|
||||
@@ -8,6 +8,7 @@ import MassargOption, {
|
||||
MassargHelpFlag,
|
||||
} from './option'
|
||||
import { setOrPush } from './utils'
|
||||
import MassargExample, { ExampleConfig } from './example'
|
||||
|
||||
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
z.object({
|
||||
@@ -54,8 +55,9 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
description: string
|
||||
aliases: string[]
|
||||
private _run?: Runner<Args>
|
||||
options: MassargOption[] = []
|
||||
commands: MassargCommand<any>[] = []
|
||||
options: MassargOption[] = []
|
||||
examples: MassargExample[] = []
|
||||
args: Partial<Args> = {}
|
||||
|
||||
constructor(options: CommandConfig<Args>) {
|
||||
@@ -79,6 +81,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
): MassargCommand<Args> {
|
||||
try {
|
||||
const command = config instanceof MassargCommand ? config : new MassargCommand(config)
|
||||
const existing = this.commands.find((c) => c.name === command.name)
|
||||
if (existing) {
|
||||
throw new ValidationError({
|
||||
code: 'duplicate_command',
|
||||
message: `Command "${command.name}" already exists`,
|
||||
path: [this.name, command.name],
|
||||
})
|
||||
}
|
||||
this.commands.push(command)
|
||||
return this
|
||||
} catch (e) {
|
||||
@@ -93,11 +103,23 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
flag(config: Omit<OptionConfig<boolean>, 'parse'>): MassargCommand<Args>
|
||||
flag(config: Omit<OptionConfig<boolean>, 'parse' | 'isDefault'>): MassargCommand<Args>
|
||||
flag(config: MassargFlag): MassargCommand<Args>
|
||||
flag(config: Omit<OptionConfig<boolean>, 'parse'> | MassargFlag): MassargCommand<Args> {
|
||||
flag(
|
||||
config: Omit<OptionConfig<boolean>, 'parse' | 'isDefault'> | MassargFlag,
|
||||
): MassargCommand<Args> {
|
||||
try {
|
||||
const flag = config instanceof MassargFlag ? config : new MassargFlag(config)
|
||||
if (flag.isDefault) {
|
||||
const defaultOption = this.options.find((o) => o.isDefault)
|
||||
if (defaultOption) {
|
||||
throw new ValidationError({
|
||||
code: 'duplicate_default_option',
|
||||
message: `Option "${flag.name}" cannot be set as default because option "${defaultOption.name}" is already set as default`,
|
||||
path: [this.name, flag.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
this.options.push(flag as MassargOption)
|
||||
return this
|
||||
} catch (e) {
|
||||
@@ -118,6 +140,16 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
try {
|
||||
const option =
|
||||
config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
|
||||
if (option.isDefault) {
|
||||
const defaultOption = this.options.find((o) => o.isDefault)
|
||||
if (defaultOption) {
|
||||
throw new ValidationError({
|
||||
code: 'duplicate_default_option',
|
||||
message: `Option "${option.name}" cannot be set as default because option "${defaultOption.name}" is already set as default`,
|
||||
path: [this.name, option.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
this.options.push(option as MassargOption)
|
||||
return this
|
||||
} catch (e) {
|
||||
@@ -132,6 +164,11 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
example(config: ExampleConfig): MassargCommand<Args> {
|
||||
this.examples.push(new MassargExample(config))
|
||||
return this
|
||||
}
|
||||
|
||||
main<A extends ArgsObject = Args>(run: Runner<A>): MassargCommand<Args> {
|
||||
this._run = run
|
||||
return this
|
||||
|
||||
124
src/example.ts
124
src/example.ts
@@ -1,98 +1,34 @@
|
||||
import { massarg } from '.'
|
||||
import MassargCommand from './command'
|
||||
import { ParseError } from './error'
|
||||
import z from 'zod'
|
||||
import { ValidationError } from './error'
|
||||
|
||||
type A = { test: boolean }
|
||||
const echoCmd = massarg<any>({
|
||||
name: 'echo',
|
||||
description: 'Echo back the arguments',
|
||||
aliases: ['e'],
|
||||
run: (opts) => {
|
||||
console.log('Echoing back', opts)
|
||||
},
|
||||
export const ExampleConfig = z.object({
|
||||
description: z.string().optional(),
|
||||
input: z.string().optional(),
|
||||
output: z.string().optional(),
|
||||
})
|
||||
const addCmd = massarg<{ component: string }>({
|
||||
name: 'add',
|
||||
description: 'Add a component',
|
||||
aliases: ['a'],
|
||||
run: (opts, parser) => {
|
||||
parser.printHelp()
|
||||
console.log('Adding component', opts.component)
|
||||
},
|
||||
})
|
||||
.option({
|
||||
name: 'component',
|
||||
description:
|
||||
'Component to add. Ut consectetur eu et occaecat enim magna amet eiusmod laboris deserunt proident culpa nulla ipsum adipiscing ullamco laboris sed est',
|
||||
aliases: ['c'],
|
||||
// aliases: "" as never,
|
||||
})
|
||||
.option({
|
||||
name: 'classes',
|
||||
description: 'Classes to add',
|
||||
aliases: ['l'],
|
||||
array: true,
|
||||
})
|
||||
.option({
|
||||
name: 'custom',
|
||||
description: 'Custom option',
|
||||
aliases: ['x'],
|
||||
parse: (value) => {
|
||||
const asNumber = Number(value)
|
||||
if (isNaN(asNumber)) {
|
||||
throw new ParseError({
|
||||
path: ['custom'],
|
||||
message: 'Custom option must be a number',
|
||||
code: 'invalid_number',
|
||||
})
|
||||
}
|
||||
return {
|
||||
value: asNumber,
|
||||
half: asNumber / 2,
|
||||
double: asNumber * 2,
|
||||
}
|
||||
},
|
||||
})
|
||||
export type ExampleConfig = z.infer<typeof ExampleConfig>
|
||||
|
||||
const removeCmd = new MassargCommand<{ component: string }>({
|
||||
name: 'remove',
|
||||
description: 'Remove a component',
|
||||
aliases: ['r'],
|
||||
run: (opts) => {
|
||||
console.log('Removing component', opts.component)
|
||||
},
|
||||
}).option({
|
||||
name: 'component',
|
||||
description: 'Component to remove',
|
||||
aliases: ['c'],
|
||||
})
|
||||
export default class MassargExample {
|
||||
description: string | undefined
|
||||
input: string | undefined
|
||||
output: string | undefined
|
||||
|
||||
const args = massarg<A>({
|
||||
name: 'my-cli',
|
||||
description: 'This is an example CLI',
|
||||
bindHelpOption: true,
|
||||
bindHelpCommand: true,
|
||||
})
|
||||
.main((opts, parser) => {
|
||||
console.log('Main command - printing all opts')
|
||||
console.log(opts, '\n')
|
||||
parser.printHelp()
|
||||
})
|
||||
.command(echoCmd)
|
||||
.command(addCmd)
|
||||
.command(removeCmd)
|
||||
.flag({
|
||||
name: 'bool',
|
||||
description: 'Example boolean option',
|
||||
aliases: ['b'],
|
||||
})
|
||||
.option({
|
||||
name: 'number',
|
||||
description: 'Example number option',
|
||||
aliases: ['n'],
|
||||
type: 'number',
|
||||
})
|
||||
|
||||
// console.log("Opts:", args.getArgs(process.argv.slice(2)), "\n")
|
||||
|
||||
args.parse(process.argv.slice(2))
|
||||
constructor(config: ExampleConfig) {
|
||||
ExampleConfig.parse(config)
|
||||
if (
|
||||
config.description === undefined &&
|
||||
config.input === undefined &&
|
||||
config.output === undefined
|
||||
) {
|
||||
throw new ValidationError({
|
||||
code: 'invalid_example',
|
||||
message: 'Example must have at least one of description, input, or output',
|
||||
path: ['example'],
|
||||
})
|
||||
}
|
||||
this.description = config.description
|
||||
this.input = config.input
|
||||
this.output = config.output
|
||||
}
|
||||
}
|
||||
export { MassargExample }
|
||||
|
||||
135
src/help.ts
135
src/help.ts
@@ -1,5 +1,6 @@
|
||||
import { format, StringStyle, stripColors } from './color'
|
||||
import MassargCommand from './command'
|
||||
import { chainStr, indent } from './utils'
|
||||
|
||||
export type GenerateTableCommandConfig = {
|
||||
maxRowLength?: number
|
||||
@@ -25,6 +26,11 @@ export type GenerateHelpOptions = {
|
||||
descriptionStyle?: StringStyle
|
||||
subtitleStyle?: StringStyle
|
||||
usageStyle?: StringStyle
|
||||
exampleStyles?: {
|
||||
description?: StringStyle
|
||||
input?: StringStyle
|
||||
output?: StringStyle
|
||||
}
|
||||
}
|
||||
|
||||
export type HelpItem = {
|
||||
@@ -61,40 +67,80 @@ export class HelpGenerator {
|
||||
aliasPrefix: '',
|
||||
...this.config.commandOptions,
|
||||
})
|
||||
const examples = entry.examples
|
||||
.map((example) => {
|
||||
const { description, input, output } = example
|
||||
return chainStr(
|
||||
description && [
|
||||
format(description, {
|
||||
reset: true,
|
||||
bold: true,
|
||||
...this.config.exampleStyles?.description,
|
||||
}),
|
||||
'',
|
||||
],
|
||||
input &&
|
||||
format(input, { reset: true, color: 'yellow', ...this.config.exampleStyles?.input }),
|
||||
output &&
|
||||
format(output, {
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
...this.config.exampleStyles?.output,
|
||||
}),
|
||||
)
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return chainStr(
|
||||
format(entry.name, {
|
||||
bold: true,
|
||||
color: 'brightWhite',
|
||||
reset: true,
|
||||
...this.config.titleStyle,
|
||||
}),
|
||||
'',
|
||||
format(entry.description, { reset: true, ...this.config.descriptionStyle }),
|
||||
commands.length && [
|
||||
'',
|
||||
format(`Commands for ${entry.name}:`, {
|
||||
return (
|
||||
chainStr(
|
||||
format(`Usage: ${entry.name} [...options]`, {
|
||||
bold: true,
|
||||
color: 'yellow',
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
...this.config.titleStyle,
|
||||
}),
|
||||
'',
|
||||
commands,
|
||||
],
|
||||
options.length && [
|
||||
'',
|
||||
format(`Options for ${entry.name}:`, {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
'',
|
||||
options,
|
||||
],
|
||||
format(entry.description, { reset: true, ...this.config.descriptionStyle }),
|
||||
commands.length &&
|
||||
indent([
|
||||
'',
|
||||
format(`Commands for ${entry.name}:`, {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
'',
|
||||
indent(commands),
|
||||
]),
|
||||
options.length &&
|
||||
indent([
|
||||
'',
|
||||
format(`Options for ${entry.name}:`, {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
'',
|
||||
indent(options),
|
||||
]),
|
||||
examples.length &&
|
||||
indent([
|
||||
'',
|
||||
format('Examples:', {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: 'brightWhite',
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
'',
|
||||
indent(examples),
|
||||
]),
|
||||
) + '\n'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,7 +168,7 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||
})
|
||||
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
|
||||
const nameStyle = (name: string) =>
|
||||
format(name, { bold: true, color: 'brightWhite', reset: true, ...config.nameStyle })
|
||||
format(name, { color: 'yellow', reset: true, ...config.nameStyle })
|
||||
const descStyle = (desc: string) =>
|
||||
format(desc, { color: 'gray', reset: true, ...config.descriptionStyle })
|
||||
const table = rows.map((row) => {
|
||||
@@ -157,32 +203,3 @@ function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||
|
||||
return table.join('\n')
|
||||
}
|
||||
|
||||
type Parseable = string | number | boolean | Record<string, unknown>
|
||||
|
||||
function chainStr(...strs: (Parseable | Parseable[])[]) {
|
||||
const res: string[] = []
|
||||
for (const str of strs) {
|
||||
if (typeof str === 'string') {
|
||||
res.push(str)
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(str)) {
|
||||
res.push(chainStr(...str))
|
||||
continue
|
||||
}
|
||||
if (typeof str === 'object') {
|
||||
for (const [key, value] of Object.entries(str)) {
|
||||
if (Boolean(value)) {
|
||||
res.push(key)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (Boolean(str)) {
|
||||
res.push(str.toString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return res.join('\n')
|
||||
}
|
||||
|
||||
103
src/sample.ts
Normal file
103
src/sample.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { massarg } from '.'
|
||||
import MassargCommand from './command'
|
||||
import { ParseError } from './error'
|
||||
|
||||
type A = { test: boolean }
|
||||
const echoCmd = massarg<any>({
|
||||
name: 'echo',
|
||||
description: 'Echo back the arguments',
|
||||
aliases: ['e'],
|
||||
run: (opts) => {
|
||||
console.log('Echoing back', opts)
|
||||
},
|
||||
})
|
||||
const addCmd = massarg<{ component: string }>({
|
||||
name: 'add',
|
||||
description: 'Add a component',
|
||||
aliases: ['a'],
|
||||
run: (opts, parser) => {
|
||||
parser.printHelp()
|
||||
console.log('Adding component', opts.component)
|
||||
},
|
||||
})
|
||||
.option({
|
||||
name: 'component',
|
||||
description:
|
||||
'Component to add. Ut consectetur eu et occaecat enim magna amet eiusmod laboris deserunt proident culpa nulla ipsum adipiscing ullamco laboris sed est',
|
||||
aliases: ['c'],
|
||||
// aliases: "" as never,
|
||||
})
|
||||
.option({
|
||||
name: 'classes',
|
||||
description: 'Classes to add',
|
||||
aliases: ['l'],
|
||||
array: true,
|
||||
})
|
||||
.option({
|
||||
name: 'custom',
|
||||
description: 'Custom option',
|
||||
aliases: ['x'],
|
||||
parse: (value) => {
|
||||
const asNumber = Number(value)
|
||||
if (isNaN(asNumber)) {
|
||||
throw new ParseError({
|
||||
path: ['custom'],
|
||||
message: 'Custom option must be a number',
|
||||
code: 'invalid_number',
|
||||
})
|
||||
}
|
||||
return {
|
||||
value: asNumber,
|
||||
half: asNumber / 2,
|
||||
double: asNumber * 2,
|
||||
}
|
||||
},
|
||||
})
|
||||
.example({
|
||||
description: 'Add a component',
|
||||
input: 'my-cli add foo',
|
||||
output: 'Adding component foo',
|
||||
})
|
||||
|
||||
const removeCmd = new MassargCommand<{ component: string }>({
|
||||
name: 'remove',
|
||||
description: 'Remove a component',
|
||||
aliases: ['r'],
|
||||
run: (opts) => {
|
||||
console.log('Removing component', opts.component)
|
||||
},
|
||||
}).option({
|
||||
name: 'component',
|
||||
description: 'Component to remove',
|
||||
aliases: ['c'],
|
||||
})
|
||||
|
||||
const main = massarg<A>({
|
||||
name: 'my-cli',
|
||||
description: 'This is an example CLI',
|
||||
bindHelpOption: true,
|
||||
bindHelpCommand: true,
|
||||
})
|
||||
.main((opts, parser) => {
|
||||
console.log('Main command - printing all opts')
|
||||
console.log(opts, '\n')
|
||||
parser.printHelp()
|
||||
})
|
||||
.command(echoCmd)
|
||||
.command(addCmd)
|
||||
.command(removeCmd)
|
||||
.flag({
|
||||
name: 'bool',
|
||||
description: 'Example boolean option',
|
||||
aliases: ['b'],
|
||||
})
|
||||
.option({
|
||||
name: 'number',
|
||||
description: 'Example number option',
|
||||
aliases: ['n'],
|
||||
type: 'number',
|
||||
})
|
||||
|
||||
// console.log("Opts:", main.getArgs(process.argv.slice(2)), "\n")
|
||||
|
||||
main.parse(process.argv.slice(2))
|
||||
38
src/utils.ts
38
src/utils.ts
@@ -8,3 +8,41 @@ export function setOrPush<T>(
|
||||
}
|
||||
return newValue as T
|
||||
}
|
||||
type Parseable = string | number | boolean | null | undefined | Record<string, unknown>
|
||||
|
||||
export function chainStr(...strs: (Parseable | Parseable[])[]) {
|
||||
const res: string[] = []
|
||||
for (const str of strs) {
|
||||
if (typeof str === 'string') {
|
||||
res.push(str)
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(str)) {
|
||||
res.push(chainStr(...str))
|
||||
continue
|
||||
}
|
||||
if (str == null) {
|
||||
continue
|
||||
}
|
||||
if (typeof str === 'object') {
|
||||
for (const [key, value] of Object.entries(str)) {
|
||||
if (Boolean(value)) {
|
||||
res.push(key)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (Boolean(str)) {
|
||||
res.push(str.toString())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return res.join('\n')
|
||||
}
|
||||
|
||||
export function indent(str: Parseable | Parseable[], indent = 2): string {
|
||||
return chainStr(str)
|
||||
.split('\n')
|
||||
.map((s) => ' '.repeat(indent) + s)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"src/example.ts"
|
||||
"src/sample.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user