feat: built-in help command + flag

This commit is contained in:
2023-11-19 13:37:12 +02:00
committed by Chen Asraf
parent 4051864429
commit f0ee853dbe
3 changed files with 36 additions and 9 deletions

View File

@@ -1,7 +1,12 @@
import { z } from "zod"
import { isZodError, ValidationError } from "./error"
import { isZodError, ParseError, ValidationError } from "./error"
import { HelpGenerator } from "./help"
import MassargOption, { MassargFlag, OptionConfig, TypedOptionConfig } from "./option"
import MassargOption, {
MassargFlag,
OptionConfig,
TypedOptionConfig,
MassargHelpFlag,
} from "./option"
import { setOrPush } from "./utils"
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
@@ -13,6 +18,9 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
.function()
.args(args, z.any())
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<Runner<z.infer<RunArgs>>>,
bindHelpCommand: z.boolean().optional(),
bindHelpOption: z.boolean().optional(),
// argsHint: z.string().optional(),
})
export type CommandConfig<T = unknown> = z.infer<ReturnType<typeof CommandConfig<z.ZodType<T>>>>
@@ -39,6 +47,12 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
this.description = options.description
this.aliases = options.aliases ?? []
this._run = options.run
if (options.bindHelpCommand) {
this.command(new MassargHelpCommand())
}
if (options.bindHelpOption) {
this.option(new MassargHelpFlag())
}
}
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
@@ -122,7 +136,12 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
if (command) {
return command.parse(_argv, this.args, parent ?? this)
}
// TODO pass all un-handled args to an "args" option
const defaultOption = this.options.find((o) => o.isDefault)
if (defaultOption) {
console.log("Parsing default option")
_argv = this.parseOption(`--${defaultOption.name}`, [arg, ..._argv])
continue
}
}
if (this._run) {
this._run({ ...args, ...this.args } as Args, parent ?? this)
@@ -138,6 +157,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
message: "Unknown option",
})
}
console.log("parseOption", [arg, ...argv])
const res = option._parseDetails([arg, ...argv])
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
res.value,
@@ -189,11 +209,12 @@ 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">>) {
constructor(config: Partial<Omit<CommandConfig<T>, "run">> = {}) {
super({
name: "help",
aliases: ["h"],
description: "Print help",
description: "Print help for this command, or a subcommand if specified",
// argsHint: "[command]",
run: (args, parent) => {
if (args.command) {
const command = parent.commands.find((c) => c.name === args.command)
@@ -201,10 +222,11 @@ export class MassargHelpCommand<T extends ArgsObject = ArgsObject> extends Massa
command.printHelp()
return
} else {
throw new ValidationError({
throw new ParseError({
path: ["command"],
code: "unknown_command",
message: "Unknown command",
received: args.command,
})
}
}

View File

@@ -62,6 +62,8 @@ const removeCmd = new MassargCommand<{ component: string }>({
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")

View File

@@ -49,6 +49,7 @@ export default class MassargOption<T = unknown> {
aliases: string[]
parse: (value: string) => T
isArray: boolean
isDefault: boolean
constructor(options: OptionConfig<T>) {
OptionConfig(z.any()).parse(options)
@@ -58,6 +59,7 @@ export default class MassargOption<T = unknown> {
this.aliases = options.aliases
this.parse = options.parse ?? ((x) => x as unknown as T)
this.isArray = options.array ?? false
this.isDefault = options.isDefault ?? false
}
static fromTypedConfig<T = unknown>(config: TypedOptionConfig<T>): MassargOption<T> {
@@ -70,10 +72,8 @@ export default class MassargOption<T = unknown> {
_parseDetails(argv: string[]): ArgvValue<T> {
// TODO: support --option=value
argv.shift()
let input = ""
try {
input = argv.shift()!
if (!this._match(argv[0])) {
throw new ParseError({
path: [this.name],
@@ -82,6 +82,8 @@ export default class MassargOption<T = unknown> {
received: JSON.stringify(argv[0]),
})
}
argv.shift()
input = argv.shift()!
const value = this.parse(input)
return { key: this.name, value, argv }
} catch (e) {
@@ -103,6 +105,7 @@ export default class MassargOption<T = unknown> {
}
_match(arg: string): boolean {
if (!arg) return false
// full prefix
if (arg.startsWith(OPT_FULL_PREFIX)) {
// negate full prefix
@@ -225,7 +228,7 @@ export class MassargFlag extends MassargOption<boolean> {
}
export class MassargHelpFlag extends MassargFlag {
constructor(config: Partial<Omit<OptionConfig<boolean>, "parse">>) {
constructor(config: Partial<Omit<OptionConfig<boolean>, "parse">> = {}) {
super({
name: "help",
description: "Show this help message",