mirror of
https://github.com/chenasraf/massarg.git
synced 2026-05-18 01:39:05 +00:00
feat: pass main instance to run fn
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 120,
|
||||
"printWidth": 100,
|
||||
"singleQuote": false
|
||||
}
|
||||
|
||||
110
src/command.ts
110
src/command.ts
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { ValidationError } from "./error"
|
||||
import MassargOption, { MassargFlag, MassargNumber, OptionConfig, TypedOptionConfig } from "./option"
|
||||
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError } from "./utils"
|
||||
import Massarg from "./massarg"
|
||||
import MassargOption, { TypedOptionConfig } from "./option"
|
||||
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError, setOrPush } from "./utils"
|
||||
|
||||
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
z.object({
|
||||
@@ -10,8 +11,10 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
|
||||
aliases: z.string().array().optional(),
|
||||
run: z
|
||||
.function()
|
||||
.args(args)
|
||||
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<(args: z.infer<RunArgs>) => Promise<void> | void>,
|
||||
.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
|
||||
>,
|
||||
})
|
||||
|
||||
export type CommandConfig<T = unknown> = z.infer<ReturnType<typeof CommandConfig<z.ZodType<T>>>>
|
||||
@@ -24,7 +27,10 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
name: string
|
||||
description: string
|
||||
aliases: string[]
|
||||
private _run?: (options: Args) => Promise<void> | void
|
||||
private _run?: <P extends ArgsObject = Args>(
|
||||
options: Args,
|
||||
instance: Massarg<P>,
|
||||
) => Promise<void> | void
|
||||
options: MassargOption[] = []
|
||||
commands: MassargCommand<any>[] = []
|
||||
args: Partial<Args> = {}
|
||||
@@ -34,12 +40,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
this.name = options.name
|
||||
this.description = options.description
|
||||
this.aliases = options.aliases ?? []
|
||||
this._run = options.run
|
||||
this._run = options.run as typeof this._run
|
||||
}
|
||||
|
||||
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
|
||||
command<A extends ArgsObject = Args>(config: MassargCommand<A>): MassargCommand<Args>
|
||||
command<A extends ArgsObject = Args>(config: CommandConfig<A> | MassargCommand<A>): MassargCommand<Args> {
|
||||
command<A extends ArgsObject = Args>(
|
||||
config: CommandConfig<A> | MassargCommand<A>,
|
||||
): MassargCommand<Args> {
|
||||
try {
|
||||
const command = config instanceof MassargCommand ? config : new MassargCommand(config)
|
||||
this.commands.push(command)
|
||||
@@ -47,7 +55,7 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
} catch (e) {
|
||||
if (isZodError(e)) {
|
||||
throw new ValidationError({
|
||||
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
code: e.issues[0].code,
|
||||
message: e.issues[0].message,
|
||||
})
|
||||
@@ -60,13 +68,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
option<T = string>(config: TypedOptionConfig<T>): MassargCommand<Args>
|
||||
option<T = string>(config: TypedOptionConfig<T> | MassargOption<T>): MassargCommand<Args> {
|
||||
try {
|
||||
const option = config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
|
||||
const option =
|
||||
config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
|
||||
this.options.push(option as MassargOption)
|
||||
return this
|
||||
} catch (e) {
|
||||
if (isZodError(e)) {
|
||||
throw new ValidationError({
|
||||
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
code: e.issues[0].code,
|
||||
message: e.issues[0].message,
|
||||
})
|
||||
@@ -75,65 +84,84 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
}
|
||||
}
|
||||
|
||||
main(run: (options: Args) => Promise<void> | void): MassargCommand<Args> {
|
||||
this._run = run
|
||||
main<A extends ArgsObject = Args>(
|
||||
run: (options: Args, instance: MassargCommand<A>) => Promise<void> | void,
|
||||
): MassargCommand<Args> {
|
||||
this._run = run as typeof this._run
|
||||
return this
|
||||
}
|
||||
|
||||
parse(argv: string[], args?: Partial<Args>): Promise<void> | void {
|
||||
console.log("parse:", this.name)
|
||||
console.log(argv)
|
||||
parse(argv: string[], args?: Partial<Args>, parent?: MassargCommand<Args>): Promise<void> | void {
|
||||
this.args ??= {}
|
||||
this.args = { ...this.args, ...args }
|
||||
let _argv = [...argv]
|
||||
while (_argv.length) {
|
||||
const arg = _argv.shift()!
|
||||
console.log("parsing:", arg, _argv)
|
||||
const found = this.options.some((o) => o._isOption(arg))
|
||||
if (found) {
|
||||
console.log("option:", arg, _argv)
|
||||
_argv = this.parseOption(arg, _argv)
|
||||
continue
|
||||
}
|
||||
|
||||
const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
|
||||
if (command) {
|
||||
console.log("command:", arg, _argv)
|
||||
return command.parse(_argv, this.args)
|
||||
return command.parse(_argv, this.args, parent ?? this)
|
||||
}
|
||||
// TODO pass all un-handled args to an "args" option
|
||||
console.log("Nothing to do", arg, _argv)
|
||||
}
|
||||
if (this._run) {
|
||||
console.log("run:", this.args)
|
||||
this._run({ ...args, ...this.args } as Args)
|
||||
this._run({ ...args, ...this.args } as Args, parent ?? this)
|
||||
}
|
||||
}
|
||||
|
||||
private parseOption(arg: string, argv: string[]): string[] {
|
||||
private parseOption(arg: string, argv: string[]) {
|
||||
const option = this.options.find((o) => o._match(arg))
|
||||
|
||||
if (!option) {
|
||||
// TODO create custom error object
|
||||
throw new Error(`Unknown option ${arg}`)
|
||||
throw new ValidationError({
|
||||
path: [arg],
|
||||
code: "unknown_option",
|
||||
message: "Unknown option",
|
||||
})
|
||||
}
|
||||
const res = option.valueFromArgv([arg, ...argv])
|
||||
console.log("option class name", option.constructor.name)
|
||||
if (option.isArray) {
|
||||
this.args[res.key as keyof Args] ??= [] as Args[keyof Args]
|
||||
const _a = this.args[res.key as keyof Args] as unknown[]
|
||||
_a.push(res.value) as Args[keyof Args]
|
||||
} else {
|
||||
this.args[res.key as keyof Args] = res.value as Args[keyof Args]
|
||||
}
|
||||
console.log("option response:", { value: res.value, argv: res.argv })
|
||||
const res = option._parseDetails([arg, ...argv])
|
||||
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
|
||||
res.value,
|
||||
this.args[res.key as keyof Args],
|
||||
option.isArray,
|
||||
)
|
||||
return res.argv
|
||||
}
|
||||
|
||||
getArgs(argv: string[]): Args {
|
||||
console.log("getArgs:", this.name)
|
||||
console.log(argv)
|
||||
return {} as Args
|
||||
let args: Args = {} as Args
|
||||
let _argv = [...argv]
|
||||
while (_argv.length) {
|
||||
const arg = _argv.shift()!
|
||||
const found = this.options.some((o) => o._isOption(arg))
|
||||
if (found) {
|
||||
const option = this.options.find((o) => o._match(arg))
|
||||
if (!option) {
|
||||
throw new ValidationError({
|
||||
path: [arg],
|
||||
code: "unknown_option",
|
||||
message: "Unknown option",
|
||||
})
|
||||
}
|
||||
const res = option._parseDetails(argv)
|
||||
args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
|
||||
res.value,
|
||||
args[res.key as keyof Args],
|
||||
option.isArray,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
|
||||
if (command) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
helpString(): string {
|
||||
@@ -151,6 +179,10 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
|
||||
.filter((s) => typeof s === "string")
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
printHelp(): void {
|
||||
console.log(this.helpString())
|
||||
}
|
||||
}
|
||||
|
||||
export { MassargCommand }
|
||||
|
||||
19
src/error.ts
19
src/error.ts
@@ -16,13 +16,28 @@ export class ParseError extends Error {
|
||||
path: string[]
|
||||
code: string
|
||||
message: string
|
||||
received: unknown
|
||||
|
||||
constructor({ path, code, message }: { path: string[]; code: string; message: string }) {
|
||||
const msg = `${path.join(".")}: ${message}`
|
||||
constructor({
|
||||
path,
|
||||
code,
|
||||
message,
|
||||
received,
|
||||
}: {
|
||||
path: string[]
|
||||
code: string
|
||||
message: string
|
||||
received?: unknown
|
||||
}) {
|
||||
let msg = `${path.join(".")}: ${message}`
|
||||
if (received) {
|
||||
msg += ` (received: ${received})`
|
||||
}
|
||||
super(msg)
|
||||
this.path = path
|
||||
this.code = code
|
||||
this.message = msg
|
||||
this.name = "ParseError"
|
||||
this.received = received
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ const args = massarg<A>({
|
||||
name: "my-cli",
|
||||
description: "This is an example CLI",
|
||||
})
|
||||
// .main((opts) => {
|
||||
// console.log("Main command - printing all opts")
|
||||
// console.log(opts)
|
||||
// })
|
||||
.main((opts, parser) => {
|
||||
console.log("Main command - printing all opts")
|
||||
console.log(opts, "\n")
|
||||
parser.printHelp()
|
||||
})
|
||||
.command(
|
||||
massarg<{ component: string }>({
|
||||
name: "add",
|
||||
@@ -70,7 +71,7 @@ const args = massarg<A>({
|
||||
)
|
||||
.option({
|
||||
name: "bool",
|
||||
description: "Example number option",
|
||||
description: "Example boolean option",
|
||||
aliases: ["b"],
|
||||
type: "boolean",
|
||||
})
|
||||
@@ -83,6 +84,6 @@ const args = massarg<A>({
|
||||
|
||||
const opts = args.getArgs(process.argv.slice(2))
|
||||
|
||||
console.log("Opts:", opts)
|
||||
console.log("Opts:", opts, "\n")
|
||||
|
||||
args.parse(process.argv.slice(2))
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class Massarg<Args extends ArgsObject = ArgsObject> extends Massa
|
||||
export { Massarg }
|
||||
|
||||
export function massarg<Args extends ArgsObject = ArgsObject>(
|
||||
options: MinimalCommandConfig<Args>,
|
||||
options: MinimalCommandConfig<Args>
|
||||
): MassargCommand<Args> {
|
||||
return new Massarg(options)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ export const TypedOptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
type: z.enum(["string", "number", "boolean"]).optional(),
|
||||
}),
|
||||
)
|
||||
export type TypedOptionConfig<T = unknown> = z.infer<ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>>
|
||||
export type TypedOptionConfig<T = unknown> = z.infer<
|
||||
ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>
|
||||
>
|
||||
|
||||
export const ArrayOptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
TypedOptionConfig(z.array(type)).merge(
|
||||
@@ -27,8 +29,11 @@ export const ArrayOptionConfig = <T extends z.ZodType>(type: T) =>
|
||||
defaultValue: z.array(type).optional(),
|
||||
}),
|
||||
)
|
||||
export type ArrayOptionConfig<T = unknown> = z.infer<ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>>
|
||||
export type ArrayOptionConfig<T = unknown> = z.infer<
|
||||
ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>
|
||||
>
|
||||
|
||||
// TODO turn to options
|
||||
const OPT_FULL_PREFIX = "--"
|
||||
const OPT_SHORT_PREFIX = "-"
|
||||
const NEGATE_FULL_PREFIX = "no-"
|
||||
@@ -64,11 +69,13 @@ export default class MassargOption<T = unknown> {
|
||||
return new MassargOption(config as OptionConfig<T>)
|
||||
}
|
||||
|
||||
valueFromArgv(argv: string[]): ArgvValue<T> {
|
||||
_parseDetails(argv: string[]): ArgvValue<T> {
|
||||
// TODO: support --option=value
|
||||
argv.shift()
|
||||
let input = ""
|
||||
try {
|
||||
const value = this.parse(argv.shift()!)
|
||||
input = argv.shift()!
|
||||
const value = this.parse(input)
|
||||
return { key: this.name, value, argv }
|
||||
} catch (e) {
|
||||
if (isZodError(e)) {
|
||||
@@ -76,6 +83,7 @@ export default class MassargOption<T = unknown> {
|
||||
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
code: e.issues[0].code,
|
||||
message: e.issues[0].message,
|
||||
received: JSON.stringify(input),
|
||||
})
|
||||
}
|
||||
throw e
|
||||
@@ -109,7 +117,11 @@ export default class MassargOption<T = unknown> {
|
||||
}
|
||||
|
||||
_isOption(arg: string): boolean {
|
||||
return arg.startsWith(OPT_FULL_PREFIX) || arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)
|
||||
return (
|
||||
arg.startsWith(OPT_FULL_PREFIX) ||
|
||||
arg.startsWith(OPT_SHORT_PREFIX) ||
|
||||
arg.startsWith(NEGATE_SHORT_PREFIX)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,14 +133,15 @@ export class MassargNumber extends MassargOption<number> {
|
||||
})
|
||||
}
|
||||
|
||||
valueFromArgv(argv: string[]): ArgvValue<number> {
|
||||
_parseDetails(argv: string[]): ArgvValue<number> {
|
||||
try {
|
||||
const { argv: _argv, value } = super.valueFromArgv(argv)
|
||||
const { argv: _argv, value } = super._parseDetails(argv)
|
||||
if (isNaN(value)) {
|
||||
throw new ParseError({
|
||||
path: [this.name],
|
||||
code: "invalid_type",
|
||||
message: "Expected a number",
|
||||
received: JSON.stringify(argv[0]),
|
||||
})
|
||||
}
|
||||
return { key: this.name, value, argv: _argv }
|
||||
@@ -138,6 +151,7 @@ export class MassargNumber extends MassargOption<number> {
|
||||
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
|
||||
code: e.issues[0].code,
|
||||
message: e.issues[0].message,
|
||||
received: JSON.stringify(argv[0]),
|
||||
})
|
||||
}
|
||||
throw e
|
||||
@@ -153,7 +167,7 @@ export class MassargFlag extends MassargOption<boolean> {
|
||||
})
|
||||
}
|
||||
|
||||
valueFromArgv(argv: string[]): ArgvValue<boolean> {
|
||||
_parseDetails(argv: string[]): ArgvValue<boolean> {
|
||||
try {
|
||||
const isNegation = argv[0]?.startsWith("^")
|
||||
argv.shift()
|
||||
|
||||
21
src/utils.ts
21
src/utils.ts
@@ -50,7 +50,10 @@ export function generateHelpTable(
|
||||
return table.join("\n")
|
||||
}
|
||||
|
||||
export function generateOptionsHelpTable(options: MassargOption<unknown>[], config?: GenerateTableOptions): string {
|
||||
export function generateOptionsHelpTable(
|
||||
options: MassargOption<unknown>[],
|
||||
config?: GenerateTableOptions,
|
||||
): string {
|
||||
return generateHelpTable(options, {
|
||||
namePrefix: "--",
|
||||
aliasPrefix: "-",
|
||||
@@ -58,10 +61,24 @@ export function generateOptionsHelpTable(options: MassargOption<unknown>[], conf
|
||||
})
|
||||
}
|
||||
|
||||
export function generateCommandsHelpTable(commands: MassargCommand[], config?: GenerateTableOptions): string {
|
||||
export function generateCommandsHelpTable(
|
||||
commands: MassargCommand[],
|
||||
config?: GenerateTableOptions,
|
||||
): string {
|
||||
return generateHelpTable(commands, {
|
||||
namePrefix: "",
|
||||
aliasPrefix: "",
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
export function setOrPush<T>(
|
||||
newValue: unknown,
|
||||
currentValue: T[] | T | undefined,
|
||||
isArray: boolean,
|
||||
): T {
|
||||
if (isArray) {
|
||||
return [...((currentValue as unknown[]) ?? []), newValue] as T
|
||||
}
|
||||
return newValue as T
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user