Skip to content

Commit 29d6973

Browse files
committed
feat: pass main instance to run fn
1 parent 73c1ad9 commit 29d6973

7 files changed

Lines changed: 138 additions & 59 deletions

File tree

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"semi": false,
3-
"printWidth": 120,
3+
"printWidth": 100,
44
"singleQuote": false
55
}

src/command.ts

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { z } from "zod"
22
import { ValidationError } from "./error"
3-
import MassargOption, { MassargFlag, MassargNumber, OptionConfig, TypedOptionConfig } from "./option"
4-
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError } from "./utils"
3+
import Massarg from "./massarg"
4+
import MassargOption, { TypedOptionConfig } from "./option"
5+
import { generateCommandsHelpTable, generateOptionsHelpTable, isZodError, setOrPush } from "./utils"
56

67
export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
78
z.object({
@@ -10,8 +11,10 @@ export const CommandConfig = <RunArgs extends z.ZodType>(args: RunArgs) =>
1011
aliases: z.string().array().optional(),
1112
run: z
1213
.function()
13-
.args(args)
14-
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<(args: z.infer<RunArgs>) => Promise<void> | void>,
14+
.args(args, z.any())
15+
.returns(z.union([z.promise(z.void()), z.void()])) as z.ZodType<
16+
(args: z.infer<RunArgs>, instance: MassargCommand<z.infer<RunArgs>>) => Promise<void> | void
17+
>,
1518
})
1619

1720
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> {
2427
name: string
2528
description: string
2629
aliases: string[]
27-
private _run?: (options: Args) => Promise<void> | void
30+
private _run?: <P extends ArgsObject = Args>(
31+
options: Args,
32+
instance: Massarg<P>,
33+
) => Promise<void> | void
2834
options: MassargOption[] = []
2935
commands: MassargCommand<any>[] = []
3036
args: Partial<Args> = {}
@@ -34,20 +40,22 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
3440
this.name = options.name
3541
this.description = options.description
3642
this.aliases = options.aliases ?? []
37-
this._run = options.run
43+
this._run = options.run as typeof this._run
3844
}
3945

4046
command<A extends ArgsObject = Args>(config: CommandConfig<A>): MassargCommand<Args>
4147
command<A extends ArgsObject = Args>(config: MassargCommand<A>): MassargCommand<Args>
42-
command<A extends ArgsObject = Args>(config: CommandConfig<A> | MassargCommand<A>): MassargCommand<Args> {
48+
command<A extends ArgsObject = Args>(
49+
config: CommandConfig<A> | MassargCommand<A>,
50+
): MassargCommand<Args> {
4351
try {
4452
const command = config instanceof MassargCommand ? config : new MassargCommand(config)
4553
this.commands.push(command)
4654
return this
4755
} catch (e) {
4856
if (isZodError(e)) {
4957
throw new ValidationError({
50-
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
58+
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
5159
code: e.issues[0].code,
5260
message: e.issues[0].message,
5361
})
@@ -60,13 +68,14 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
6068
option<T = string>(config: TypedOptionConfig<T>): MassargCommand<Args>
6169
option<T = string>(config: TypedOptionConfig<T> | MassargOption<T>): MassargCommand<Args> {
6270
try {
63-
const option = config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
71+
const option =
72+
config instanceof MassargOption ? config : MassargOption.fromTypedConfig(config)
6473
this.options.push(option as MassargOption)
6574
return this
6675
} catch (e) {
6776
if (isZodError(e)) {
6877
throw new ValidationError({
69-
path: [config.name, ...e.issues[0].path.map((p) => p.toString())],
78+
path: [this.name, config.name, ...e.issues[0].path.map((p) => p.toString())],
7079
code: e.issues[0].code,
7180
message: e.issues[0].message,
7281
})
@@ -75,65 +84,84 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
7584
}
7685
}
7786

78-
main(run: (options: Args) => Promise<void> | void): MassargCommand<Args> {
79-
this._run = run
87+
main<A extends ArgsObject = Args>(
88+
run: (options: Args, instance: MassargCommand<A>) => Promise<void> | void,
89+
): MassargCommand<Args> {
90+
this._run = run as typeof this._run
8091
return this
8192
}
8293

83-
parse(argv: string[], args?: Partial<Args>): Promise<void> | void {
84-
console.log("parse:", this.name)
85-
console.log(argv)
94+
parse(argv: string[], args?: Partial<Args>, parent?: MassargCommand<Args>): Promise<void> | void {
8695
this.args ??= {}
8796
this.args = { ...this.args, ...args }
8897
let _argv = [...argv]
8998
while (_argv.length) {
9099
const arg = _argv.shift()!
91-
console.log("parsing:", arg, _argv)
92100
const found = this.options.some((o) => o._isOption(arg))
93101
if (found) {
94-
console.log("option:", arg, _argv)
95102
_argv = this.parseOption(arg, _argv)
96103
continue
97104
}
98105

99106
const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
100107
if (command) {
101-
console.log("command:", arg, _argv)
102-
return command.parse(_argv, this.args)
108+
return command.parse(_argv, this.args, parent ?? this)
103109
}
104110
// TODO pass all un-handled args to an "args" option
105-
console.log("Nothing to do", arg, _argv)
106111
}
107112
if (this._run) {
108-
console.log("run:", this.args)
109-
this._run({ ...args, ...this.args } as Args)
113+
this._run({ ...args, ...this.args } as Args, parent ?? this)
110114
}
111115
}
112116

113-
private parseOption(arg: string, argv: string[]): string[] {
117+
private parseOption(arg: string, argv: string[]) {
114118
const option = this.options.find((o) => o._match(arg))
115-
116119
if (!option) {
117-
// TODO create custom error object
118-
throw new Error(`Unknown option ${arg}`)
119-
}
120-
const res = option.valueFromArgv([arg, ...argv])
121-
console.log("option class name", option.constructor.name)
122-
if (option.isArray) {
123-
this.args[res.key as keyof Args] ??= [] as Args[keyof Args]
124-
const _a = this.args[res.key as keyof Args] as unknown[]
125-
_a.push(res.value) as Args[keyof Args]
126-
} else {
127-
this.args[res.key as keyof Args] = res.value as Args[keyof Args]
120+
throw new ValidationError({
121+
path: [arg],
122+
code: "unknown_option",
123+
message: "Unknown option",
124+
})
128125
}
129-
console.log("option response:", { value: res.value, argv: res.argv })
126+
const res = option._parseDetails([arg, ...argv])
127+
this.args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
128+
res.value,
129+
this.args[res.key as keyof Args],
130+
option.isArray,
131+
)
130132
return res.argv
131133
}
132134

133135
getArgs(argv: string[]): Args {
134-
console.log("getArgs:", this.name)
135-
console.log(argv)
136-
return {} as Args
136+
let args: Args = {} as Args
137+
let _argv = [...argv]
138+
while (_argv.length) {
139+
const arg = _argv.shift()!
140+
const found = this.options.some((o) => o._isOption(arg))
141+
if (found) {
142+
const option = this.options.find((o) => o._match(arg))
143+
if (!option) {
144+
throw new ValidationError({
145+
path: [arg],
146+
code: "unknown_option",
147+
message: "Unknown option",
148+
})
149+
}
150+
const res = option._parseDetails(argv)
151+
args[res.key as keyof Args] = setOrPush<Args[keyof Args]>(
152+
res.value,
153+
args[res.key as keyof Args],
154+
option.isArray,
155+
)
156+
continue
157+
}
158+
159+
const command = this.commands.find((c) => c.name === arg || c.aliases.includes(arg))
160+
if (command) {
161+
break
162+
}
163+
}
164+
return args
137165
}
138166

139167
helpString(): string {
@@ -151,6 +179,10 @@ export default class MassargCommand<Args extends ArgsObject = ArgsObject> {
151179
.filter((s) => typeof s === "string")
152180
.join("\n")
153181
}
182+
183+
printHelp(): void {
184+
console.log(this.helpString())
185+
}
154186
}
155187

156188
export { MassargCommand }

src/error.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,28 @@ export class ParseError extends Error {
1616
path: string[]
1717
code: string
1818
message: string
19+
received: unknown
1920

20-
constructor({ path, code, message }: { path: string[]; code: string; message: string }) {
21-
const msg = `${path.join(".")}: ${message}`
21+
constructor({
22+
path,
23+
code,
24+
message,
25+
received,
26+
}: {
27+
path: string[]
28+
code: string
29+
message: string
30+
received?: unknown
31+
}) {
32+
let msg = `${path.join(".")}: ${message}`
33+
if (received) {
34+
msg += ` (received: ${received})`
35+
}
2236
super(msg)
2337
this.path = path
2438
this.code = code
2539
this.message = msg
2640
this.name = "ParseError"
41+
this.received = received
2742
}
2843
}

src/example.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const args = massarg<A>({
77
name: "my-cli",
88
description: "This is an example CLI",
99
})
10-
// .main((opts) => {
11-
// console.log("Main command - printing all opts")
12-
// console.log(opts)
13-
// })
10+
.main((opts, parser) => {
11+
console.log("Main command - printing all opts")
12+
console.log(opts, "\n")
13+
parser.printHelp()
14+
})
1415
.command(
1516
massarg<{ component: string }>({
1617
name: "add",
@@ -70,7 +71,7 @@ const args = massarg<A>({
7071
)
7172
.option({
7273
name: "bool",
73-
description: "Example number option",
74+
description: "Example boolean option",
7475
aliases: ["b"],
7576
type: "boolean",
7677
})
@@ -83,6 +84,6 @@ const args = massarg<A>({
8384

8485
const opts = args.getArgs(process.argv.slice(2))
8586

86-
console.log("Opts:", opts)
87+
console.log("Opts:", opts, "\n")
8788

8889
args.parse(process.argv.slice(2))

src/massarg.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class Massarg<Args extends ArgsObject = ArgsObject> extends Massa
1919
export { Massarg }
2020

2121
export function massarg<Args extends ArgsObject = ArgsObject>(
22-
options: MinimalCommandConfig<Args>,
22+
options: MinimalCommandConfig<Args>
2323
): MassargCommand<Args> {
2424
return new Massarg(options)
2525
}

src/option.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ export const TypedOptionConfig = <T extends z.ZodType>(type: T) =>
1919
type: z.enum(["string", "number", "boolean"]).optional(),
2020
}),
2121
)
22-
export type TypedOptionConfig<T = unknown> = z.infer<ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>>
22+
export type TypedOptionConfig<T = unknown> = z.infer<
23+
ReturnType<typeof TypedOptionConfig<z.ZodType<T>>>
24+
>
2325

2426
export const ArrayOptionConfig = <T extends z.ZodType>(type: T) =>
2527
TypedOptionConfig(z.array(type)).merge(
2628
z.object({
2729
defaultValue: z.array(type).optional(),
2830
}),
2931
)
30-
export type ArrayOptionConfig<T = unknown> = z.infer<ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>>
32+
export type ArrayOptionConfig<T = unknown> = z.infer<
33+
ReturnType<typeof ArrayOptionConfig<z.ZodType<T>>>
34+
>
3135

36+
// TODO turn to options
3237
const OPT_FULL_PREFIX = "--"
3338
const OPT_SHORT_PREFIX = "-"
3439
const NEGATE_FULL_PREFIX = "no-"
@@ -64,18 +69,21 @@ export default class MassargOption<T = unknown> {
6469
return new MassargOption(config as OptionConfig<T>)
6570
}
6671

67-
valueFromArgv(argv: string[]): ArgvValue<T> {
72+
_parseDetails(argv: string[]): ArgvValue<T> {
6873
// TODO: support --option=value
6974
argv.shift()
75+
let input = ""
7076
try {
71-
const value = this.parse(argv.shift()!)
77+
input = argv.shift()!
78+
const value = this.parse(input)
7279
return { key: this.name, value, argv }
7380
} catch (e) {
7481
if (isZodError(e)) {
7582
throw new ParseError({
7683
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
7784
code: e.issues[0].code,
7885
message: e.issues[0].message,
86+
received: JSON.stringify(input),
7987
})
8088
}
8189
throw e
@@ -109,7 +117,11 @@ export default class MassargOption<T = unknown> {
109117
}
110118

111119
_isOption(arg: string): boolean {
112-
return arg.startsWith(OPT_FULL_PREFIX) || arg.startsWith(OPT_SHORT_PREFIX) || arg.startsWith(NEGATE_SHORT_PREFIX)
120+
return (
121+
arg.startsWith(OPT_FULL_PREFIX) ||
122+
arg.startsWith(OPT_SHORT_PREFIX) ||
123+
arg.startsWith(NEGATE_SHORT_PREFIX)
124+
)
113125
}
114126
}
115127

@@ -121,14 +133,15 @@ export class MassargNumber extends MassargOption<number> {
121133
})
122134
}
123135

124-
valueFromArgv(argv: string[]): ArgvValue<number> {
136+
_parseDetails(argv: string[]): ArgvValue<number> {
125137
try {
126-
const { argv: _argv, value } = super.valueFromArgv(argv)
138+
const { argv: _argv, value } = super._parseDetails(argv)
127139
if (isNaN(value)) {
128140
throw new ParseError({
129141
path: [this.name],
130142
code: "invalid_type",
131143
message: "Expected a number",
144+
received: JSON.stringify(argv[0]),
132145
})
133146
}
134147
return { key: this.name, value, argv: _argv }
@@ -138,6 +151,7 @@ export class MassargNumber extends MassargOption<number> {
138151
path: [this.name, ...e.issues[0].path.map((p) => p.toString())],
139152
code: e.issues[0].code,
140153
message: e.issues[0].message,
154+
received: JSON.stringify(argv[0]),
141155
})
142156
}
143157
throw e
@@ -153,7 +167,7 @@ export class MassargFlag extends MassargOption<boolean> {
153167
})
154168
}
155169

156-
valueFromArgv(argv: string[]): ArgvValue<boolean> {
170+
_parseDetails(argv: string[]): ArgvValue<boolean> {
157171
try {
158172
const isNegation = argv[0]?.startsWith("^")
159173
argv.shift()

0 commit comments

Comments
 (0)