mirror of
https://github.com/chenasraf/massarg.git
synced 2026-05-18 01:39:05 +00:00
feat: help generator
This commit is contained in:
11
package.json
11
package.json
@@ -22,13 +22,12 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/node": "^16.18.61",
|
||||
"jest": "^27.5.1",
|
||||
"ts-jest": "^27.1.5",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.9.2",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
|
||||
1417
pnpm-lock.yaml
generated
1417
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
45
src/color.ts
Normal file
45
src/color.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const ansiStyles = {
|
||||
reset: "\x1b[0m",
|
||||
bold: "\x1b[1m",
|
||||
underline: "\x1b[4m",
|
||||
black: "\x1b[30m",
|
||||
}
|
||||
|
||||
export const ansiColors = {
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m", // warning
|
||||
blue: "\x1b[34m",
|
||||
magenta: "\x1b[35m", // error
|
||||
cyan: "\x1b[36m",
|
||||
white: "\x1b[37m",
|
||||
gray: "\x1b[90m",
|
||||
grey: "\x1b[90m",
|
||||
brightRed: "\x1b[91m",
|
||||
brightGreen: "\x1b[92m",
|
||||
brightYellow: "\x1b[93m",
|
||||
brightBlue: "\x1b[94m",
|
||||
brightMagenta: "\x1b[95m",
|
||||
brightCyan: "\x1b[96m",
|
||||
brightWhite: "\x1b[97m",
|
||||
}
|
||||
|
||||
export type StringStyle = {
|
||||
color?: keyof typeof ansiColors
|
||||
bold?: boolean
|
||||
underline?: boolean
|
||||
reset?: boolean
|
||||
}
|
||||
|
||||
export function format(string: string, style: StringStyle = {}): string {
|
||||
const { color, bold, underline, reset } = style
|
||||
const colorCode = color ? ansiColors[color] : ""
|
||||
const boldCode = bold ? ansiStyles.bold : ""
|
||||
const underlineCode = underline ? ansiStyles.underline : ""
|
||||
const resetCode = reset ? ansiStyles.reset : ""
|
||||
return `${colorCode}${boldCode}${underlineCode}${string}${resetCode}`
|
||||
}
|
||||
|
||||
export function stripColors(string: string): string {
|
||||
return string.replace(/\x1b\[\d+m/g, "")
|
||||
}
|
||||
100
src/command.ts
100
src/command.ts
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { ValidationError } from "./error"
|
||||
import Massarg from "./massarg"
|
||||
import MassargOption, { TypedOptionConfig } from "./option"
|
||||
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError, setOrPush } from "./utils"
|
||||
import { isZodError, ValidationError } from "./error"
|
||||
import { HelpGenerator } from "./help"
|
||||
import MassargOption, { MassargFlag, OptionConfig, TypedOptionConfig } from "./option"
|
||||
import { setOrPush } from "./utils"
|
||||
|
||||
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
z.object({
|
||||
@@ -12,25 +12,23 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
run: z
|
||||
.function()
|
||||
.args(args, z.any())
|
||||
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<
|
||||
(args: z.infer<RunArgs>, instance: MassargCommand<z.infer<RunArgs>>) => Promise<void> | void
|
||||
>,
|
||||
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<Runner<z.infer<RunArgs>>>,
|
||||
})
|
||||
|
||||
export type CommandConfig<T = unknown> = z.infer<ReturnType<typeof CommandConfig<z.ZodType<T>>>>
|
||||
|
||||
export type ArgsObject = Record<string, unknown>
|
||||
|
||||
// export type RunFn<Args extends ArgsObject> = (options: Args) => Promise<void> | void
|
||||
export type Runner<Args extends ArgsObject> = <A extends ArgsObject = Args>(
|
||||
options: A,
|
||||
instance: MassargCommand<A>,
|
||||
) => Promise<void> | void
|
||||
|
||||
export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
name: string
|
||||
description: string
|
||||
aliases: string[]
|
||||
private _run?: <P extends ArgsObject = Args>(
|
||||
options: Args,
|
||||
instance: Massarg<P>,
|
||||
) => Promise<void> | void
|
||||
private _run?: Runner<Args>
|
||||
options: MassargOption[] = []
|
||||
commands: MassargCommand<any>[] = []
|
||||
args: Partial<Args> = {}
|
||||
@@ -40,7 +38,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
this.name = options.name
|
||||
this.description = options.description
|
||||
this.aliases = options.aliases ?? []
|
||||
this._run = options.run as typeof this._run
|
||||
this._run = options.run
|
||||
}
|
||||
|
||||
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
|
||||
@@ -64,6 +62,25 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
flag(config: Omit<OptionConfig<boolean>, "parse">): MassargCommand<Args>
|
||||
flag(config: MassargFlag): MassargCommand<Args>
|
||||
flag(config: Omit<OptionConfig<boolean>, "parse"> | MassargFlag): MassargCommand<Args> {
|
||||
try {
|
||||
const flag = config instanceof MassargFlag ? config : new MassargFlag(config)
|
||||
this.options.push(flag as MassargOption)
|
||||
return this
|
||||
} catch (e) {
|
||||
if (isZodError(e)) {
|
||||
throw new ValidationError({
|
||||
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
code: e.issues[0].code,
|
||||
message: e.issues[0].message,
|
||||
})
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
option<T = string>(config: MassargOption<T>): MassargCommand<Args>
|
||||
option<T = string>(config: TypedOptionConfig<T>): MassargCommand<Args>
|
||||
option<T = string>(config: TypedOptionConfig<T> | MassargOption<T>): MassargCommand<Args> {
|
||||
@@ -84,10 +101,8 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
main<A extends ArgsObject = Args>(
|
||||
run: (options: Args, instance: MassargCommand<A>) => Promise<void> | void,
|
||||
): MassargCommand<Args> {
|
||||
this._run = run as typeof this._run
|
||||
main<A extends ArgsObject = Args>(run: Runner<A>): MassargCommand<Args> {
|
||||
this._run = run
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -118,7 +133,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
const option = this.options.find((o) => o._match(arg))
|
||||
if (!option) {
|
||||
throw new ValidationError({
|
||||
path: [arg],
|
||||
path: [MassargOption.getName(arg)],
|
||||
code: "unknown_option",
|
||||
message: "Unknown option",
|
||||
})
|
||||
@@ -142,7 +157,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
const option = this.options.find((o) => o._match(arg))
|
||||
if (!option) {
|
||||
throw new ValidationError({
|
||||
path: [arg],
|
||||
path: [MassargOption.getName(arg)],
|
||||
code: "unknown_option",
|
||||
message: "Unknown option",
|
||||
})
|
||||
@@ -165,19 +180,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
|
||||
helpString(): string {
|
||||
const options = generateOptionsHelpTable(this.options)
|
||||
const commands = generateCommandsHelpTable(this.commands)
|
||||
return [
|
||||
`${this.name} - ${this.description}`,
|
||||
commands.length && "",
|
||||
commands.length && `Commands for ${this.name}:`,
|
||||
commands.length && commands,
|
||||
options.length && "",
|
||||
options.length && `Options for ${this.name}:`,
|
||||
options.length && options,
|
||||
]
|
||||
.filter((s) => typeof s === "string")
|
||||
.join("\n")
|
||||
return new HelpGenerator(this).generate()
|
||||
}
|
||||
|
||||
printHelp(): void {
|
||||
@@ -185,4 +188,37 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
export class MassargHelpCommand<T extends ArgsObject = ArgsObject> extends MassargCommand<T> {
|
||||
constructor(config: Partial<Omit<CommandConfig<T>, "run">>) {
|
||||
super({
|
||||
name: "help",
|
||||
aliases: ["h"],
|
||||
description: "Print help",
|
||||
run: (args, parent) => {
|
||||
if (args.command) {
|
||||
const command = parent.commands.find((c) => c.name === args.command)
|
||||
if (command) {
|
||||
command.printHelp()
|
||||
return
|
||||
} else {
|
||||
throw new ValidationError({
|
||||
path: ["command"],
|
||||
code: "unknown_command",
|
||||
message: "Unknown command",
|
||||
})
|
||||
}
|
||||
}
|
||||
parent.printHelp()
|
||||
},
|
||||
...config,
|
||||
})
|
||||
this.option({
|
||||
name: "command",
|
||||
aliases: ["c"],
|
||||
description: "Command to print help for",
|
||||
isDefault: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { MassargCommand }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export class ValidationError extends Error {
|
||||
path: string[]
|
||||
code: string
|
||||
@@ -41,3 +43,7 @@ export class ParseError extends Error {
|
||||
this.received = received
|
||||
}
|
||||
}
|
||||
|
||||
export function isZodError(e: unknown): e is z.ZodError {
|
||||
return e instanceof z.ZodError
|
||||
}
|
||||
|
||||
122
src/example.ts
122
src/example.ts
@@ -3,6 +3,62 @@ import MassargCommand from "./command"
|
||||
import { ParseError } from "./error"
|
||||
|
||||
type A = { test: boolean }
|
||||
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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
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 args = massarg<A>({
|
||||
name: "my-cli",
|
||||
description: "This is an example CLI",
|
||||
@@ -12,68 +68,12 @@ const args = massarg<A>({
|
||||
console.log(opts, "\n")
|
||||
parser.printHelp()
|
||||
})
|
||||
.command(
|
||||
massarg<{ component: string }>({
|
||||
name: "add",
|
||||
description: "Add a component",
|
||||
aliases: ["a"],
|
||||
run: (opts) => {
|
||||
console.log("Adding component", opts.component)
|
||||
},
|
||||
})
|
||||
.option({
|
||||
name: "component",
|
||||
description: "Component to add",
|
||||
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,
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
.command(
|
||||
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"],
|
||||
// aliases: "" as never,
|
||||
}),
|
||||
)
|
||||
.option({
|
||||
.command(addCmd)
|
||||
.command(removeCmd)
|
||||
.flag({
|
||||
name: "bool",
|
||||
description: "Example boolean option",
|
||||
aliases: ["b"],
|
||||
type: "boolean",
|
||||
})
|
||||
.option({
|
||||
name: "number",
|
||||
@@ -82,8 +82,6 @@ const args = massarg<A>({
|
||||
type: "number",
|
||||
})
|
||||
|
||||
const opts = args.getArgs(process.argv.slice(2))
|
||||
|
||||
console.log("Opts:", opts, "\n")
|
||||
// console.log("Opts:", args.getArgs(process.argv.slice(2)), "\n")
|
||||
|
||||
args.parse(process.argv.slice(2))
|
||||
|
||||
188
src/help.ts
Normal file
188
src/help.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { format, StringStyle, stripColors } from "./color"
|
||||
import MassargCommand from "./command"
|
||||
|
||||
export type GenerateTableCommandConfig = {
|
||||
maxRowLength?: number
|
||||
namePrefix?: string
|
||||
aliasPrefix?: string
|
||||
compact?: boolean
|
||||
nameStyle?: StringStyle
|
||||
descriptionStyle?: StringStyle
|
||||
}
|
||||
|
||||
export type GenerateTableOptionConfig = GenerateTableCommandConfig & {
|
||||
typeStyle?: StringStyle
|
||||
defaultStyle?: StringStyle
|
||||
}
|
||||
|
||||
export type GenerateHelpOptions = {
|
||||
// sub-styles
|
||||
commandOptions?: GenerateTableCommandConfig
|
||||
optionOptions?: GenerateTableOptionConfig
|
||||
|
||||
// global styles
|
||||
titleStyle?: StringStyle
|
||||
descriptionStyle?: StringStyle
|
||||
subtitleStyle?: StringStyle
|
||||
usageStyle?: StringStyle
|
||||
}
|
||||
|
||||
export type HelpItem = {
|
||||
name: string
|
||||
aliases: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export class HelpGenerator {
|
||||
entry: MassargCommand<any>
|
||||
config: GenerateHelpOptions
|
||||
|
||||
constructor(entry: MassargCommand<any>, config?: GenerateHelpOptions) {
|
||||
this.entry = entry
|
||||
this.config = {
|
||||
commandOptions: {
|
||||
...config?.commandOptions,
|
||||
},
|
||||
optionOptions: {
|
||||
...config?.optionOptions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
generate(): string {
|
||||
const entry = this.entry
|
||||
const options = generateHelpTable(entry.options, {
|
||||
namePrefix: "--",
|
||||
aliasPrefix: "-",
|
||||
...this.config.optionOptions,
|
||||
})
|
||||
const commands = generateHelpTable(entry.commands, {
|
||||
namePrefix: "",
|
||||
aliasPrefix: "",
|
||||
...this.config.commandOptions,
|
||||
})
|
||||
|
||||
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}:`, {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: "brightWhite",
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
"",
|
||||
commands,
|
||||
],
|
||||
options.length && [
|
||||
"",
|
||||
format(`Options for ${entry.name}:`, {
|
||||
bold: true,
|
||||
reset: true,
|
||||
color: "brightWhite",
|
||||
underline: true,
|
||||
...this.config.subtitleStyle,
|
||||
}),
|
||||
"",
|
||||
options,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
printHelp(): void {
|
||||
console.log(this.generate())
|
||||
}
|
||||
}
|
||||
|
||||
function generateHelpTable<T extends Partial<GenerateTableCommandConfig>>(
|
||||
items: HelpItem[],
|
||||
{
|
||||
maxRowLength = 80,
|
||||
namePrefix = "",
|
||||
aliasPrefix = "",
|
||||
compact = false,
|
||||
...config
|
||||
}: Partial<T> = {},
|
||||
): string {
|
||||
const rows = items.map((o) => {
|
||||
const name = `${namePrefix}${o.name}${
|
||||
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ""
|
||||
}`
|
||||
const description = o.description
|
||||
return { name, description }
|
||||
})
|
||||
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
|
||||
const nameStyle = (name: string) =>
|
||||
format(name, { bold: true, color: "brightWhite", reset: true, ...config.nameStyle })
|
||||
const descStyle = (desc: string) =>
|
||||
format(desc, { color: "gray", reset: true, ...config.descriptionStyle })
|
||||
const table = rows.map((row) => {
|
||||
const name = nameStyle(row.name.padEnd(maxNameLength + 2))
|
||||
const description = descStyle(row.description)
|
||||
const length = stripColors(name).length + stripColors(description).length
|
||||
if (length <= maxRowLength) {
|
||||
const line = `${name}${description}`
|
||||
if (!compact) {
|
||||
return `${line}\n`
|
||||
}
|
||||
return line
|
||||
}
|
||||
const subRows: string[] = []
|
||||
const words = description.split(" ")
|
||||
let currentRow = name
|
||||
|
||||
for (const word of words) {
|
||||
if (stripColors(currentRow).length + stripColors(word).length + 1 > maxRowLength) {
|
||||
subRows.push(currentRow)
|
||||
currentRow = " ".repeat(maxNameLength + 2)
|
||||
}
|
||||
currentRow += `${word} `
|
||||
}
|
||||
|
||||
if (!compact) {
|
||||
subRows.push("")
|
||||
}
|
||||
|
||||
return subRows.join("\n")
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { ParseError } from "./error"
|
||||
import { isZodError } from "./utils"
|
||||
import { isZodError, ParseError } from "./error"
|
||||
|
||||
export const OptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
z.object({
|
||||
@@ -10,13 +9,15 @@ export const OptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
aliases: z.string().array(),
|
||||
parse: z.function().args(z.string()).returns(type).optional(),
|
||||
array: z.boolean().optional(),
|
||||
required: z.boolean().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
})
|
||||
export type OptionConfig<T = unknown> = z.infer<ReturnType<typeof OptionConfig<z.ZodType<T>>>>
|
||||
|
||||
export const TypedOptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
OptionConfig(type).merge(
|
||||
z.object({
|
||||
type: z.enum(["string", "number", "boolean"]).optional(),
|
||||
type: z.enum(["number"]).optional(),
|
||||
}),
|
||||
)
|
||||
export type TypedOptionConfig<T = unknown> = z.infer<
|
||||
@@ -63,8 +64,6 @@ export default class MassargOption<T = unknown> {
|
||||
switch (config.type) {
|
||||
case "number":
|
||||
return new MassargNumber(config as OptionConfig<number>) as MassargOption<T>
|
||||
case "boolean":
|
||||
return new MassargFlag(config) as MassargOption<T>
|
||||
}
|
||||
return new MassargOption(config as OptionConfig<T>)
|
||||
}
|
||||
@@ -75,6 +74,14 @@ export default class MassargOption<T = unknown> {
|
||||
let input = ""
|
||||
try {
|
||||
input = argv.shift()!
|
||||
if (!this._match(argv[0])) {
|
||||
throw new ParseError({
|
||||
path: [this.name],
|
||||
code: "invalid_option",
|
||||
message: `Expected option ${this.name}`,
|
||||
received: JSON.stringify(argv[0]),
|
||||
})
|
||||
}
|
||||
const value = this.parse(input)
|
||||
return { key: this.name, value, argv }
|
||||
} catch (e) {
|
||||
@@ -123,6 +130,25 @@ export default class MassargOption<T = unknown> {
|
||||
arg.startsWith(NEGATE_SHORT_PREFIX)
|
||||
)
|
||||
}
|
||||
|
||||
static getName(arg: string): string {
|
||||
if (arg.startsWith(OPT_FULL_PREFIX)) {
|
||||
// negate full prefix
|
||||
if (arg.startsWith(`--${NEGATE_FULL_PREFIX}`)) {
|
||||
return arg.slice(`--${NEGATE_FULL_PREFIX}`.length)
|
||||
}
|
||||
return arg.slice(OPT_FULL_PREFIX.length)
|
||||
}
|
||||
// short prefix
|
||||
if (arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)) {
|
||||
return arg.slice(OPT_SHORT_PREFIX.length)
|
||||
}
|
||||
// negate short prefix
|
||||
if (arg.startsWith(NEGATE_SHORT_PREFIX)) {
|
||||
return arg.slice(NEGATE_SHORT_PREFIX.length)
|
||||
}
|
||||
return "<blank>"
|
||||
}
|
||||
}
|
||||
|
||||
export class MassargNumber extends MassargOption<number> {
|
||||
@@ -169,7 +195,17 @@ export class MassargFlag extends MassargOption<boolean> {
|
||||
|
||||
_parseDetails(argv: string[]): ArgvValue<boolean> {
|
||||
try {
|
||||
const isNegation = argv[0]?.startsWith("^")
|
||||
const isNegation =
|
||||
argv[0]?.startsWith(NEGATE_SHORT_PREFIX) || argv[0]?.startsWith(NEGATE_FULL_PREFIX)
|
||||
if (!this._match(argv[0])) {
|
||||
throw new ParseError({
|
||||
path: [this.name],
|
||||
code: "invalid_option",
|
||||
message: `Expected option ${this.name}`,
|
||||
received: JSON.stringify(argv[0]),
|
||||
})
|
||||
}
|
||||
|
||||
argv.shift()
|
||||
if (isNegation) {
|
||||
return { key: this.name, value: false, argv }
|
||||
@@ -188,4 +224,15 @@ export class MassargFlag extends MassargOption<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
export class MassargHelpFlag extends MassargFlag {
|
||||
constructor(config: Partial<Omit<OptionConfig<boolean>, "parse">>) {
|
||||
super({
|
||||
name: "help",
|
||||
description: "Show this help message",
|
||||
aliases: ["h"],
|
||||
...config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { MassargOption }
|
||||
|
||||
74
src/utils.ts
74
src/utils.ts
@@ -1,77 +1,3 @@
|
||||
import { z } from "zod"
|
||||
import MassargCommand from "./command"
|
||||
import MassargOption from "./option"
|
||||
|
||||
export function isZodError(e: unknown): e is z.ZodError {
|
||||
return e instanceof z.ZodError
|
||||
}
|
||||
|
||||
type GenerateTableOptions = {
|
||||
maxRowLength?: number
|
||||
namePrefix?: string
|
||||
aliasPrefix?: string
|
||||
}
|
||||
|
||||
/** generates an aligned table of options and their descriptions */
|
||||
export function generateHelpTable(
|
||||
items: { name: string; aliases: string[]; description: string }[],
|
||||
{ maxRowLength = 80, namePrefix = "", aliasPrefix = "" }: GenerateTableOptions = {},
|
||||
): string {
|
||||
const rows = items.map((o) => {
|
||||
const name = `${namePrefix}${o.name}${
|
||||
o.aliases.length ? ` | ${aliasPrefix}${o.aliases.join(`|${aliasPrefix}`)}` : ""
|
||||
}`
|
||||
const description = o.description
|
||||
return { name, description }
|
||||
})
|
||||
const maxNameLength = Math.max(...rows.map((o) => o.name.length))
|
||||
const table = rows.map((row) => {
|
||||
const name = row.name.padEnd(maxNameLength + 2)
|
||||
const description = row.description
|
||||
const length = name.length + description.length
|
||||
if (length <= maxRowLength) {
|
||||
return `${name}${description}`
|
||||
}
|
||||
const subRows: string[] = [name]
|
||||
const words = description.split(" ")
|
||||
let currentRow = subRows[0]
|
||||
|
||||
for (const word of words) {
|
||||
if (currentRow.length + word.length + 1 > maxRowLength) {
|
||||
subRows.push(currentRow)
|
||||
currentRow = ""
|
||||
}
|
||||
currentRow += `${word} `
|
||||
}
|
||||
|
||||
return subRows.join("\n")
|
||||
})
|
||||
|
||||
return table.join("\n")
|
||||
}
|
||||
|
||||
export function generateOptionsHelpTable(
|
||||
options: MassargOption<unknown>[],
|
||||
config?: GenerateTableOptions,
|
||||
): string {
|
||||
return generateHelpTable(options, {
|
||||
namePrefix: "--",
|
||||
aliasPrefix: "-",
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function generateCommandsHelpTable(
|
||||
commands: MassargCommand[],
|
||||
config?: GenerateTableOptions,
|
||||
): string {
|
||||
return generateHelpTable(commands, {
|
||||
namePrefix: "",
|
||||
aliasPrefix: "",
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function setOrPush<T>(
|
||||
newValue: unknown,
|
||||
currentValue: T[] | T | undefined,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
"src/sample.ts"
|
||||
"src/example.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
"target": "ES2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "Node16", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
"lib": [
|
||||
"ES2020"
|
||||
"ES2023"
|
||||
], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
|
||||
Reference in New Issue
Block a user