@silvery/commander 0.8.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -1
- package/dist/index.d.mts +805 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +719 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +20 -10
- package/src/colorize.ts +0 -104
- package/src/command.ts +0 -321
- package/src/index.ts +0 -14
- package/src/plain.ts +0 -22
- package/src/presets.ts +0 -186
- package/src/z.ts +0 -44
package/src/command.ts
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enhanced Commander Command with auto-colorized help, Standard Schema support,
|
|
3
|
-
* and array-as-choices detection.
|
|
4
|
-
*
|
|
5
|
-
* Subclasses Commander's Command so `new Command("app")` just works --
|
|
6
|
-
* it's Commander with auto-colorized help, automatic Standard Schema /
|
|
7
|
-
* legacy Zod detection, and array choices in `.option()`.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* import { Command, port, csv } from "@silvery/commander"
|
|
12
|
-
*
|
|
13
|
-
* new Command("deploy")
|
|
14
|
-
* .option("-p, --port <n>", "Port", port)
|
|
15
|
-
* .option("--tags <t>", "Tags", csv)
|
|
16
|
-
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
17
|
-
*
|
|
18
|
-
* program.parse()
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { Command as BaseCommand, Help, Option } from "commander"
|
|
23
|
-
import { colorizeHelp } from "./colorize.ts"
|
|
24
|
-
import type { StandardSchemaV1 } from "./presets.ts"
|
|
25
|
-
|
|
26
|
-
// --- Standard Schema support ---
|
|
27
|
-
|
|
28
|
-
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
29
|
-
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
30
|
-
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Wrap a Standard Schema as a Commander parser function */
|
|
34
|
-
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
35
|
-
return (value: string) => {
|
|
36
|
-
const result = schema["~standard"].validate(value)
|
|
37
|
-
if ("issues" in result) {
|
|
38
|
-
const msg = result.issues.map((i) => i.message).join(", ")
|
|
39
|
-
throw new Error(msg)
|
|
40
|
-
}
|
|
41
|
-
return result.value
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
46
|
-
|
|
47
|
-
interface ZodLike<T = any> {
|
|
48
|
-
parse(value: unknown): T
|
|
49
|
-
_def: unknown
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
53
|
-
return (
|
|
54
|
-
typeof value === "object" &&
|
|
55
|
-
value !== null &&
|
|
56
|
-
typeof (value as any).parse === "function" &&
|
|
57
|
-
"_def" in (value as any) &&
|
|
58
|
-
!("~standard" in (value as any))
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
63
|
-
return (value: string) => {
|
|
64
|
-
try {
|
|
65
|
-
return schema.parse(value)
|
|
66
|
-
} catch (err: any) {
|
|
67
|
-
if (err?.issues) {
|
|
68
|
-
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
69
|
-
throw new Error(messages)
|
|
70
|
-
}
|
|
71
|
-
throw err
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// --- Help Section Types ---
|
|
77
|
-
|
|
78
|
-
/** Position for help sections — mirrors Commander's addHelpText positions. */
|
|
79
|
-
export type HelpSectionPosition = "beforeAll" | "before" | "after" | "afterAll"
|
|
80
|
-
|
|
81
|
-
/** Content for a help section: rows of [term, description] pairs, or free-form text. */
|
|
82
|
-
export type HelpSectionContent = [string, string][] | string
|
|
83
|
-
|
|
84
|
-
// Internal storage
|
|
85
|
-
interface StoredSection {
|
|
86
|
-
position: HelpSectionPosition
|
|
87
|
-
title: string
|
|
88
|
-
content: HelpSectionContent
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Style a section term using Commander's style hooks.
|
|
93
|
-
* Splits the term into segments: option-like (-f), argument brackets (<arg>, [opt]),
|
|
94
|
-
* and command words — each styled with the appropriate hook.
|
|
95
|
-
*/
|
|
96
|
-
function styleSectionTerm(term: string, helper: any): string {
|
|
97
|
-
// Option-like terms: entire term styled as option
|
|
98
|
-
if (/^\s*-/.test(term)) return helper.styleOptionText(term)
|
|
99
|
-
|
|
100
|
-
// Mixed terms: style <arg>/[opt] as arguments, "quoted" as literal values, rest as commands
|
|
101
|
-
return term.replace(
|
|
102
|
-
/(<[^>]+>|\[[^\]]+\])|("[^"]*")|([^<["[\]]+)/g,
|
|
103
|
-
(_match, bracket: string, quoted: string, text: string) => {
|
|
104
|
-
if (bracket) return helper.styleArgumentText(bracket)
|
|
105
|
-
if (quoted) return quoted // literal values — default foreground (quotes distinguish them)
|
|
106
|
-
if (text) return helper.styleCommandText(text)
|
|
107
|
-
return ""
|
|
108
|
-
},
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export class Command extends BaseCommand {
|
|
113
|
-
private _helpSectionList: StoredSection[] = []
|
|
114
|
-
private _helpSectionsInstalled = false
|
|
115
|
-
|
|
116
|
-
constructor(name?: string) {
|
|
117
|
-
super(name)
|
|
118
|
-
colorizeHelp(this as any)
|
|
119
|
-
this._capitalizeBuiltinDescriptions()
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Add an option with smart third-argument detection.
|
|
124
|
-
*
|
|
125
|
-
* The third argument is detected in order:
|
|
126
|
-
* 1. **Array** -- treated as choices (Commander `.choices()`)
|
|
127
|
-
* 2. **Standard Schema v1** -- wrapped as a parser function
|
|
128
|
-
* 3. **Legacy Zod** (pre-3.24, has `_def` + `parse`) -- wrapped as a parser
|
|
129
|
-
* 4. **Function** -- passed through as Commander's parser function
|
|
130
|
-
* 5. **Anything else** -- passed through as a default value
|
|
131
|
-
*/
|
|
132
|
-
override option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
133
|
-
if (Array.isArray(parseArgOrDefault)) {
|
|
134
|
-
const opt = new Option(flags, description ?? "").choices(parseArgOrDefault as string[])
|
|
135
|
-
this.addOption(opt)
|
|
136
|
-
return this
|
|
137
|
-
}
|
|
138
|
-
if (isStandardSchema(parseArgOrDefault)) {
|
|
139
|
-
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue)
|
|
140
|
-
}
|
|
141
|
-
if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
142
|
-
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue)
|
|
143
|
-
}
|
|
144
|
-
if (typeof parseArgOrDefault === "function") {
|
|
145
|
-
return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
146
|
-
}
|
|
147
|
-
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Add a styled help section — like Commander's `addHelpText` but with
|
|
152
|
-
* structured content that participates in global column alignment.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* ```ts
|
|
156
|
-
* // Rows with aligned descriptions (default position: "after")
|
|
157
|
-
* program.addHelpSection("Getting Started:", [
|
|
158
|
-
* ["myapp init", "Initialize a new project"],
|
|
159
|
-
* ["myapp serve", "Start the dev server"],
|
|
160
|
-
* ])
|
|
161
|
-
*
|
|
162
|
-
* // Free-form text
|
|
163
|
-
* program.addHelpSection("Note:", "Requires Node.js 23+")
|
|
164
|
-
*
|
|
165
|
-
* // Explicit position
|
|
166
|
-
* program.addHelpSection("before", "Prerequisites:", [
|
|
167
|
-
* ["node >= 23", "Required runtime"],
|
|
168
|
-
* ])
|
|
169
|
-
* ```
|
|
170
|
-
*/
|
|
171
|
-
addHelpSection(title: string, content: HelpSectionContent): this
|
|
172
|
-
addHelpSection(position: HelpSectionPosition, title: string, content: HelpSectionContent): this
|
|
173
|
-
addHelpSection(
|
|
174
|
-
positionOrTitle: HelpSectionPosition | string,
|
|
175
|
-
titleOrContent: string | HelpSectionContent,
|
|
176
|
-
content?: HelpSectionContent,
|
|
177
|
-
): this {
|
|
178
|
-
let position: HelpSectionPosition
|
|
179
|
-
let title: string
|
|
180
|
-
let body: HelpSectionContent
|
|
181
|
-
|
|
182
|
-
if (content !== undefined) {
|
|
183
|
-
// 3-arg: addHelpSection(position, title, content)
|
|
184
|
-
position = positionOrTitle as HelpSectionPosition
|
|
185
|
-
title = titleOrContent as string
|
|
186
|
-
body = content
|
|
187
|
-
} else {
|
|
188
|
-
// 2-arg: addHelpSection(title, content) — defaults to "after"
|
|
189
|
-
position = "after"
|
|
190
|
-
title = positionOrTitle
|
|
191
|
-
body = titleOrContent as HelpSectionContent
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this._helpSectionList.push({ position, title, content: body })
|
|
195
|
-
this._installHelpSectionHooks()
|
|
196
|
-
return this
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Subcommands also get colorized help, Standard Schema, and array choices
|
|
200
|
-
override createCommand(name?: string): Command {
|
|
201
|
-
return new Command(name)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Auto-detect capitalization from user-provided descriptions and match it
|
|
206
|
-
* for built-in options (-V/--version, -h/--help).
|
|
207
|
-
*/
|
|
208
|
-
private _capitalizeBuiltinDescriptions(): void {
|
|
209
|
-
const origHelp = this.helpInformation.bind(this)
|
|
210
|
-
this.helpInformation = () => {
|
|
211
|
-
const builtinFlags = new Set(["-V, --version", "-h, --help"])
|
|
212
|
-
const userDescs = this.options
|
|
213
|
-
.filter((opt) => !builtinFlags.has(opt.flags) && opt.description && /^[a-zA-Z]/.test(opt.description))
|
|
214
|
-
.map((opt) => opt.description!)
|
|
215
|
-
for (const cmd of this.commands) {
|
|
216
|
-
if (cmd.description() && /^[a-zA-Z]/.test(cmd.description())) {
|
|
217
|
-
userDescs.push(cmd.description())
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (userDescs.length > 0) {
|
|
221
|
-
const capitalCount = userDescs.filter((d) => /^[A-Z]/.test(d)).length
|
|
222
|
-
if (capitalCount > userDescs.length / 2) {
|
|
223
|
-
for (const opt of this.options) {
|
|
224
|
-
if (builtinFlags.has(opt.flags) && opt.description && /^[a-z]/.test(opt.description)) {
|
|
225
|
-
opt.description = opt.description[0]!.toUpperCase() + opt.description.slice(1)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const helpOpt = (this as any)._helpOption
|
|
229
|
-
if (helpOpt?.description && /^[a-z]/.test(helpOpt.description)) {
|
|
230
|
-
helpOpt.description = helpOpt.description[0]!.toUpperCase() + helpOpt.description.slice(1)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return origHelp()
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Render sections for a given position using the help formatter. */
|
|
239
|
-
private _renderSections(position: HelpSectionPosition, helper: any, termWidth: number): string {
|
|
240
|
-
const sections = this._helpSectionList.filter((s) => s.position === position)
|
|
241
|
-
if (sections.length === 0) return ""
|
|
242
|
-
|
|
243
|
-
const lines: string[] = []
|
|
244
|
-
for (const section of sections) {
|
|
245
|
-
lines.push("")
|
|
246
|
-
lines.push(helper.styleTitle(section.title))
|
|
247
|
-
if (typeof section.content === "string") {
|
|
248
|
-
for (const line of section.content.split("\n")) {
|
|
249
|
-
lines.push(` ${line}`)
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
for (const [term, desc] of section.content) {
|
|
253
|
-
const styleTerm = styleSectionTerm(term, helper)
|
|
254
|
-
lines.push(helper.formatItem(styleTerm, termWidth, helper.styleDescriptionText(desc), helper))
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
lines.push("")
|
|
259
|
-
return lines.join("\n")
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Install hooks once — merges with existing configureHelp, adds addHelpText for before/afterAll. */
|
|
263
|
-
private _installHelpSectionHooks(): void {
|
|
264
|
-
if (this._helpSectionsInstalled) return
|
|
265
|
-
this._helpSectionsInstalled = true
|
|
266
|
-
|
|
267
|
-
const self = this
|
|
268
|
-
const existing = (this as any)._helpConfiguration ?? {}
|
|
269
|
-
const origPadWidth = existing.padWidth
|
|
270
|
-
const origFormatHelp = existing.formatHelp
|
|
271
|
-
const protoFormatHelp = Help.prototype.formatHelp
|
|
272
|
-
|
|
273
|
-
this.configureHelp({
|
|
274
|
-
...existing,
|
|
275
|
-
// Include section term widths in global column alignment
|
|
276
|
-
padWidth(cmd: any, helper: any) {
|
|
277
|
-
const base = origPadWidth
|
|
278
|
-
? origPadWidth(cmd, helper)
|
|
279
|
-
: Math.max(
|
|
280
|
-
helper.longestOptionTermLength(cmd, helper),
|
|
281
|
-
helper.longestGlobalOptionTermLength?.(cmd, helper) ?? 0,
|
|
282
|
-
helper.longestSubcommandTermLength(cmd, helper),
|
|
283
|
-
helper.longestArgumentTermLength(cmd, helper),
|
|
284
|
-
)
|
|
285
|
-
let sectionMax = 0
|
|
286
|
-
for (const section of self._helpSectionList) {
|
|
287
|
-
if (typeof section.content !== "string") {
|
|
288
|
-
for (const [term] of section.content) {
|
|
289
|
-
if (term.length > sectionMax) sectionMax = term.length
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return Math.max(base, sectionMax)
|
|
294
|
-
},
|
|
295
|
-
// Render "before" and "after" sections inside formatHelp
|
|
296
|
-
formatHelp(cmd: any, helper: any) {
|
|
297
|
-
const baseHelp = origFormatHelp ? origFormatHelp(cmd, helper) : protoFormatHelp.call(helper, cmd, helper)
|
|
298
|
-
const termWidth = helper.padWidth(cmd, helper)
|
|
299
|
-
const before = self._renderSections("before", helper, termWidth)
|
|
300
|
-
const after = self._renderSections("after", helper, termWidth)
|
|
301
|
-
|
|
302
|
-
// "before" goes after the Usage+Description but before Options.
|
|
303
|
-
// Since we can't inject mid-formatHelp easily, prepend before baseHelp.
|
|
304
|
-
// "after" appends at the end.
|
|
305
|
-
return (before ? before + "\n" : "") + baseHelp + after
|
|
306
|
-
},
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
// "beforeAll" and "afterAll" use Commander's addHelpText (propagates to subcommands)
|
|
310
|
-
this.addHelpText("beforeAll", () => {
|
|
311
|
-
const helper = this.createHelp()
|
|
312
|
-
const termWidth = helper.padWidth(this, helper)
|
|
313
|
-
return this._renderSections("beforeAll", helper, termWidth)
|
|
314
|
-
})
|
|
315
|
-
this.addHelpText("afterAll", () => {
|
|
316
|
-
const helper = this.createHelp()
|
|
317
|
-
const termWidth = helper.padWidth(this, helper)
|
|
318
|
-
return this._renderSections("afterAll", helper, termWidth)
|
|
319
|
-
})
|
|
320
|
-
}
|
|
321
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Enhanced Commander
|
|
2
|
-
export { Command, type HelpSectionPosition, type HelpSectionContent } from "./command.ts"
|
|
3
|
-
export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLike } from "./colorize.ts"
|
|
4
|
-
|
|
5
|
-
// Re-export Commander's other classes
|
|
6
|
-
export { Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
|
|
7
|
-
export type { OptionValues } from "commander"
|
|
8
|
-
|
|
9
|
-
// Built-in types and Standard Schema
|
|
10
|
-
export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange } from "./presets.ts"
|
|
11
|
-
export type { CLIType, StandardSchemaV1 } from "./presets.ts"
|
|
12
|
-
|
|
13
|
-
// Tree-shakeable: only evaluated if user imports z
|
|
14
|
-
export { z } from "./z.ts"
|
package/src/plain.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain Commander — Standard Schema presets without auto-colorization.
|
|
3
|
-
*
|
|
4
|
-
* Exports the base Commander Command (no auto-colorized help) plus the
|
|
5
|
-
* same typed option presets (int, uint, port, csv, intRange). Does NOT
|
|
6
|
-
* import @silvery/ansi — zero styling overhead.
|
|
7
|
-
*
|
|
8
|
-
* For colorization, import `colorizeHelp` from `@silvery/commander` (main entry).
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* import { Command, port, csv } from "@silvery/commander/plain"
|
|
13
|
-
*
|
|
14
|
-
* const program = new Command("myapp")
|
|
15
|
-
* .option("-p, --port <n>", "Port", port)
|
|
16
|
-
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export { Command } from "commander"
|
|
21
|
-
export { int, uint, port, csv, intRange } from "./presets.ts"
|
|
22
|
-
export type { StandardSchemaV1 } from "./presets.ts"
|
package/src/presets.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in CLI types — Standard Schema v1 validators for common CLI argument patterns.
|
|
3
|
-
*
|
|
4
|
-
* Zero dependencies — validation is manual, no Zod/Valibot/ArkType required.
|
|
5
|
-
* Each type implements Standard Schema v1 for interop with any schema library,
|
|
6
|
-
* plus standalone `.parse()` and `.safeParse()` convenience methods.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { Command, port, csv } from "@silvery/commander"
|
|
11
|
-
*
|
|
12
|
-
* new Command("deploy")
|
|
13
|
-
* .option("-p, --port <n>", "Port", port) // number (1-65535)
|
|
14
|
-
* .option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
15
|
-
* .option("--tags <t>", "Tags", csv) // string[]
|
|
16
|
-
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
17
|
-
*
|
|
18
|
-
* // Standalone usage (outside Commander)
|
|
19
|
-
* port.parse("3000") // 3000
|
|
20
|
-
* port.safeParse("abc") // { success: false, issues: [{ message: "..." }] }
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Standard Schema v1 interface — the universal schema interop protocol.
|
|
26
|
-
* Supports any schema library that implements Standard Schema (Zod >=3.24,
|
|
27
|
-
* Valibot >=1.0, ArkType >=2.0, etc.).
|
|
28
|
-
*
|
|
29
|
-
* Inlined to avoid any dependency on @standard-schema/spec.
|
|
30
|
-
* See: https://github.com/standard-schema/standard-schema
|
|
31
|
-
*/
|
|
32
|
-
export interface StandardSchemaV1<T = unknown> {
|
|
33
|
-
readonly "~standard": {
|
|
34
|
-
readonly version: 1
|
|
35
|
-
readonly vendor: string
|
|
36
|
-
readonly validate: (
|
|
37
|
-
value: unknown,
|
|
38
|
-
) => { value: T } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }> }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** A Standard Schema v1 CLI type with standalone parse/safeParse methods. */
|
|
43
|
-
export interface CLIType<T> extends StandardSchemaV1<T> {
|
|
44
|
-
/** Parse and validate a value, throwing on failure. */
|
|
45
|
-
parse(value: unknown): T
|
|
46
|
-
/** Parse and validate a value, returning a result object. */
|
|
47
|
-
safeParse(value: unknown): { success: true; value: T } | { success: false; issues: Array<{ message: string }> }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function createType<T>(vendor: string, validate: (value: unknown) => T): CLIType<T> {
|
|
51
|
-
const schema: CLIType<T> = {
|
|
52
|
-
"~standard": {
|
|
53
|
-
version: 1,
|
|
54
|
-
vendor,
|
|
55
|
-
validate: (value) => {
|
|
56
|
-
try {
|
|
57
|
-
return { value: validate(value) }
|
|
58
|
-
} catch (e: any) {
|
|
59
|
-
return { issues: [{ message: e.message }] }
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
parse(value: unknown): T {
|
|
64
|
-
const result = schema["~standard"].validate(value)
|
|
65
|
-
if ("issues" in result) throw new Error(result.issues[0]?.message ?? "Validation failed")
|
|
66
|
-
return result.value
|
|
67
|
-
},
|
|
68
|
-
safeParse(value: unknown) {
|
|
69
|
-
const result = schema["~standard"].validate(value)
|
|
70
|
-
if ("issues" in result) return { success: false as const, issues: [...result.issues] }
|
|
71
|
-
return { success: true as const, value: result.value }
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
return schema
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const VENDOR = "@silvery/commander"
|
|
78
|
-
|
|
79
|
-
/** Integer (coerced from string). */
|
|
80
|
-
export const int = createType<number>(VENDOR, (v) => {
|
|
81
|
-
const s = String(v).trim()
|
|
82
|
-
if (s === "") throw new Error(`Expected integer, got "${v}"`)
|
|
83
|
-
const n = Number(s)
|
|
84
|
-
if (!Number.isInteger(n)) throw new Error(`Expected integer, got "${v}"`)
|
|
85
|
-
return n
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
/** Unsigned integer (>= 0, coerced from string). */
|
|
89
|
-
export const uint = createType<number>(VENDOR, (v) => {
|
|
90
|
-
const s = String(v).trim()
|
|
91
|
-
if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
92
|
-
const n = Number(s)
|
|
93
|
-
if (!Number.isInteger(n) || n < 0) throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
94
|
-
return n
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
/** Float (coerced from string). */
|
|
98
|
-
export const float = createType<number>(VENDOR, (v) => {
|
|
99
|
-
const s = String(v).trim()
|
|
100
|
-
if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`)
|
|
101
|
-
const n = Number(s)
|
|
102
|
-
if (Number.isNaN(n)) throw new Error(`Expected number, got "${v}"`)
|
|
103
|
-
return n
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
/** Port number (1-65535). */
|
|
107
|
-
export const port = createType<number>(VENDOR, (v) => {
|
|
108
|
-
const n = Number(v)
|
|
109
|
-
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`)
|
|
110
|
-
return n
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
/** URL (validated via URL constructor). */
|
|
114
|
-
export const url = createType<string>(VENDOR, (v) => {
|
|
115
|
-
const s = String(v)
|
|
116
|
-
try {
|
|
117
|
-
new URL(s)
|
|
118
|
-
return s
|
|
119
|
-
} catch {
|
|
120
|
-
throw new Error(`Expected valid URL, got "${v}"`)
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
/** File path (non-empty string). */
|
|
125
|
-
export const path = createType<string>(VENDOR, (v) => {
|
|
126
|
-
const s = String(v)
|
|
127
|
-
if (!s) throw new Error("Expected non-empty path")
|
|
128
|
-
return s
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
/** Comma-separated values to string[]. */
|
|
132
|
-
export const csv = createType<string[]>(VENDOR, (v) => {
|
|
133
|
-
return String(v)
|
|
134
|
-
.split(",")
|
|
135
|
-
.map((s) => s.trim())
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
/** JSON string to parsed value. */
|
|
140
|
-
export const json = createType<unknown>(VENDOR, (v) => {
|
|
141
|
-
try {
|
|
142
|
-
return JSON.parse(String(v))
|
|
143
|
-
} catch {
|
|
144
|
-
throw new Error(`Expected valid JSON, got "${v}"`)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
/** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
|
|
149
|
-
export const bool = createType<boolean>(VENDOR, (v) => {
|
|
150
|
-
const s = String(v).toLowerCase()
|
|
151
|
-
if (["true", "1", "yes", "y"].includes(s)) return true
|
|
152
|
-
if (["false", "0", "no", "n"].includes(s)) return false
|
|
153
|
-
throw new Error(`Expected boolean (true/false/yes/no/1/0), got "${v}"`)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
/** Date string to Date object. */
|
|
157
|
-
export const date = createType<Date>(VENDOR, (v) => {
|
|
158
|
-
const d = new Date(String(v))
|
|
159
|
-
if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`)
|
|
160
|
-
return d
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
/** Email address (basic validation). */
|
|
164
|
-
export const email = createType<string>(VENDOR, (v) => {
|
|
165
|
-
const s = String(v)
|
|
166
|
-
if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`)
|
|
167
|
-
return s
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
/** Regex pattern string to RegExp. */
|
|
171
|
-
export const regex = createType<RegExp>(VENDOR, (v) => {
|
|
172
|
-
try {
|
|
173
|
-
return new RegExp(String(v))
|
|
174
|
-
} catch {
|
|
175
|
-
throw new Error(`Expected valid regex, got "${v}"`)
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
/** Integer with min/max bounds (factory). */
|
|
180
|
-
export function intRange(min: number, max: number): CLIType<number> {
|
|
181
|
-
return createType<number>(VENDOR, (v) => {
|
|
182
|
-
const n = Number(v)
|
|
183
|
-
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
184
|
-
return n
|
|
185
|
-
})
|
|
186
|
-
}
|
package/src/z.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extended Zod object with CLI types.
|
|
3
|
-
*
|
|
4
|
-
* Spreads all of Zod's exports and adds CLI-specific schemas.
|
|
5
|
-
* Tree-shakeable -- only evaluated if user imports `z`.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* import { z, Command } from "@silvery/commander"
|
|
10
|
-
*
|
|
11
|
-
* new Command("deploy")
|
|
12
|
-
* .option("-p, --port <n>", "Port", z.port)
|
|
13
|
-
* .option("--tags <t>", "Tags", z.csv)
|
|
14
|
-
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
15
|
-
*
|
|
16
|
-
* program.parse()
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import * as zod from "zod"
|
|
21
|
-
|
|
22
|
-
export const z = {
|
|
23
|
-
...zod,
|
|
24
|
-
// CLI types (built on Zod schemas)
|
|
25
|
-
port: zod.coerce.number().int().min(1).max(65535),
|
|
26
|
-
int: zod.coerce.number().int(),
|
|
27
|
-
uint: zod.coerce.number().int().min(0),
|
|
28
|
-
float: zod.coerce.number(),
|
|
29
|
-
csv: zod.string().transform((v: string) =>
|
|
30
|
-
v
|
|
31
|
-
.split(",")
|
|
32
|
-
.map((s: string) => s.trim())
|
|
33
|
-
.filter(Boolean),
|
|
34
|
-
),
|
|
35
|
-
url: zod.string().url(),
|
|
36
|
-
path: zod.string().min(1),
|
|
37
|
-
email: zod.string().email(),
|
|
38
|
-
date: zod.coerce.date(),
|
|
39
|
-
json: zod.string().transform((v: string) => JSON.parse(v)),
|
|
40
|
-
bool: zod
|
|
41
|
-
.enum(["true", "false", "1", "0", "yes", "no", "y", "n"] as const)
|
|
42
|
-
.transform((v: string) => ["true", "1", "yes", "y"].includes(v)),
|
|
43
|
-
intRange: (min: number, max: number) => zod.coerce.number().int().min(min).max(max),
|
|
44
|
-
}
|