@silvery/commander 0.2.0 → 0.3.0
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/README.md +67 -5
- package/package.json +12 -3
- package/src/index.ts +5 -0
- package/src/presets.ts +181 -0
- package/src/typed.ts +61 -12
package/README.md
CHANGED
|
@@ -32,6 +32,66 @@ const program = new Command("myapp").description("My CLI tool")
|
|
|
32
32
|
colorizeHelp(program) // applies recursively to all subcommands
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Presets
|
|
36
|
+
|
|
37
|
+
Pre-built validators for common CLI argument patterns. Each preset implements [Standard Schema v1](https://github.com/standard-schema/standard-schema) and works with Commander's `.option()` or standalone.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createCLI, port, csv, int, url, oneOf } from "@silvery/commander"
|
|
41
|
+
|
|
42
|
+
const cli = createCLI("deploy")
|
|
43
|
+
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
44
|
+
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
45
|
+
.option("--tags <t>", "Tags", csv) // string[]
|
|
46
|
+
.option("--callback <url>", "Callback", url) // string (validated URL)
|
|
47
|
+
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"])) // "dev" | "staging" | "prod"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Standalone usage
|
|
51
|
+
|
|
52
|
+
Presets also work outside Commander for validating env vars, config files, etc. Import from the `@silvery/commander/parse` subpath for tree-shaking:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { port, csv, oneOf } from "@silvery/commander/parse"
|
|
56
|
+
|
|
57
|
+
// .parse() — returns value or throws
|
|
58
|
+
const dbPort = port.parse(process.env.DB_PORT ?? "5432") // 3000
|
|
59
|
+
|
|
60
|
+
// .safeParse() — returns result object, never throws
|
|
61
|
+
const result = port.safeParse("abc")
|
|
62
|
+
// { success: false, issues: [{ message: 'Expected port (1-65535), got "abc"' }] }
|
|
63
|
+
|
|
64
|
+
// Standard Schema ~standard.validate() also available
|
|
65
|
+
const validated = port["~standard"].validate("8080")
|
|
66
|
+
// { value: 8080 }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Available presets
|
|
70
|
+
|
|
71
|
+
| Preset | Type | Validation |
|
|
72
|
+
| ------ | ---- | ---------- |
|
|
73
|
+
| `int` | `number` | Integer (coerced from string) |
|
|
74
|
+
| `uint` | `number` | Unsigned integer (>= 0) |
|
|
75
|
+
| `float` | `number` | Any finite number (rejects NaN) |
|
|
76
|
+
| `port` | `number` | Integer 1-65535 |
|
|
77
|
+
| `url` | `string` | Valid URL (via `URL` constructor) |
|
|
78
|
+
| `path` | `string` | Non-empty string |
|
|
79
|
+
| `csv` | `string[]` | Comma-separated, trimmed, empty filtered |
|
|
80
|
+
| `json` | `unknown` | Parsed JSON |
|
|
81
|
+
| `bool` | `boolean` | true/false/yes/no/1/0 (case-insensitive) |
|
|
82
|
+
| `date` | `Date` | Valid date string |
|
|
83
|
+
| `email` | `string` | Basic email validation (has @ and .) |
|
|
84
|
+
| `regex` | `RegExp` | Valid regex pattern |
|
|
85
|
+
|
|
86
|
+
### Factory presets
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { intRange, oneOf } from "@silvery/commander"
|
|
90
|
+
|
|
91
|
+
intRange(1, 100) // Preset<number> — integer within bounds
|
|
92
|
+
oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
|
|
93
|
+
```
|
|
94
|
+
|
|
35
95
|
## Custom parser type inference
|
|
36
96
|
|
|
37
97
|
When `.option()` is called with a parser function as the third argument, the return type is inferred:
|
|
@@ -49,9 +109,9 @@ Default values can be passed as the fourth argument:
|
|
|
49
109
|
.option("-p, --port <n>", "Port", parseInt, 8080) // → port: number (defaults to 8080)
|
|
50
110
|
```
|
|
51
111
|
|
|
52
|
-
##
|
|
112
|
+
## Standard Schema validation
|
|
53
113
|
|
|
54
|
-
Pass
|
|
114
|
+
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) compatible schema as the third argument for combined parsing, validation, and type inference. This works with Zod (>=3.24), Valibot (>=1.0), ArkType (>=2.0), and any other library implementing the standard:
|
|
55
115
|
|
|
56
116
|
```typescript
|
|
57
117
|
import { z } from "zod"
|
|
@@ -71,7 +131,7 @@ const cli = createCLI("deploy")
|
|
|
71
131
|
// → tags: string[] (transformed)
|
|
72
132
|
```
|
|
73
133
|
|
|
74
|
-
|
|
134
|
+
Schema libraries are optional peer dependencies -- detected at runtime via the Standard Schema `~standard` interface, never imported at the top level. A legacy fallback supports older Zod versions (pre-3.24) that don't implement Standard Schema yet.
|
|
75
135
|
|
|
76
136
|
## Typed action handlers
|
|
77
137
|
|
|
@@ -117,11 +177,12 @@ Chain `.env()` to set an environment variable fallback for the last-added option
|
|
|
117
177
|
| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
118
178
|
| Type inference | 1536-line .d.ts with recursive generic accumulation | ~120 lines using TS 5.4+ const type params + template literals |
|
|
119
179
|
| Custom parser types | Yes (.option with parseFloat -> number) | Yes (parser return type inference) |
|
|
120
|
-
|
|
|
180
|
+
| Standard Schema | No | Yes (Zod, Valibot, ArkType, or any Standard Schema v1 library) |
|
|
181
|
+
| Built-in presets | No | Yes (port, int, csv, url, oneOf, etc.) |
|
|
121
182
|
| Typed action handlers | Yes (full signature inference) | Yes (arguments + options) |
|
|
122
183
|
| Choices narrowing | Via .addOption() | Via .optionWithChoices() |
|
|
123
184
|
| Colorized help | Not included | Built-in via Commander's native style hooks |
|
|
124
|
-
| Package size | Types only (25 lines runtime) | Types + colorizer +
|
|
185
|
+
| Package size | Types only (25 lines runtime) | Types + colorizer + schemas (~500 lines, zero required deps) |
|
|
125
186
|
| Installation | Separate package alongside commander | Single package, re-exports Commander |
|
|
126
187
|
| Negated flags (--no-X) | Partial | Key extraction + boolean type inference |
|
|
127
188
|
|
|
@@ -129,6 +190,7 @@ Chain `.env()` to set an environment variable fallback for the last-added option
|
|
|
129
190
|
|
|
130
191
|
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
131
192
|
- [@commander-js/extra-typings](https://github.com/commander-js/extra-typings) -- inspired the type inference approach; our implementation uses modern TypeScript features (const type parameters, template literal types) to achieve similar results in fewer lines
|
|
193
|
+
- [Standard Schema](https://github.com/standard-schema/standard-schema) -- universal schema interop protocol for type-safe validation
|
|
132
194
|
- [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional ANSI color detection for respecting NO_COLOR/FORCE_COLOR/terminal capabilities
|
|
133
195
|
- Uses Commander's built-in `configureHelp()` style hooks (added in Commander 12) for colorization
|
|
134
196
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Colorized Commander.js help output using ANSI escape codes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"exports": {
|
|
24
|
-
".": "./src/index.ts"
|
|
24
|
+
".": "./src/index.ts",
|
|
25
|
+
"./parse": "./src/presets.ts"
|
|
25
26
|
},
|
|
26
27
|
"publishConfig": {
|
|
27
28
|
"access": "public"
|
|
@@ -30,7 +31,9 @@
|
|
|
30
31
|
"@commander-js/extra-typings": ">=12.0.0",
|
|
31
32
|
"@silvery/ansi": ">=0.1.0",
|
|
32
33
|
"commander": ">=12.0.0",
|
|
33
|
-
"zod": ">=3.0.0"
|
|
34
|
+
"zod": ">=3.0.0",
|
|
35
|
+
"valibot": ">=1.0.0",
|
|
36
|
+
"arktype": ">=2.0.0"
|
|
34
37
|
},
|
|
35
38
|
"peerDependenciesMeta": {
|
|
36
39
|
"commander": {
|
|
@@ -44,6 +47,12 @@
|
|
|
44
47
|
},
|
|
45
48
|
"zod": {
|
|
46
49
|
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"valibot": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"arktype": {
|
|
55
|
+
"optional": true
|
|
47
56
|
}
|
|
48
57
|
},
|
|
49
58
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type { OptionValues } from "commander"
|
|
|
4
4
|
|
|
5
5
|
// Re-export typed CLI
|
|
6
6
|
export { TypedCommand, createCLI } from "./typed.ts"
|
|
7
|
+
export type { StandardSchemaV1 } from "./typed.ts"
|
|
8
|
+
|
|
9
|
+
// Re-export presets
|
|
10
|
+
export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange, oneOf } from "./presets.ts"
|
|
11
|
+
export type { Preset } from "./presets.ts"
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Commander.js help colorization using ANSI escape codes.
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built Standard Schema v1 presets for common CLI argument patterns.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies — validation is manual, no Zod/Valibot/ArkType required.
|
|
5
|
+
* Each preset 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 { createCLI, port, csv, int, url, oneOf } from "@silvery/commander"
|
|
11
|
+
*
|
|
12
|
+
* const cli = createCLI("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("--callback <url>", "Callback", url) // string (validated URL)
|
|
17
|
+
* .option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
18
|
+
*
|
|
19
|
+
* // Standalone usage (outside Commander)
|
|
20
|
+
* port.parse("3000") // 3000
|
|
21
|
+
* port.safeParse("abc") // { success: false, issues: [{ message: "..." }] }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { StandardSchemaV1 } from "./typed.ts"
|
|
26
|
+
|
|
27
|
+
/** A Standard Schema v1 preset with standalone parse/safeParse methods. */
|
|
28
|
+
export interface Preset<T> extends StandardSchemaV1<T> {
|
|
29
|
+
/** Parse and validate a value, throwing on failure. */
|
|
30
|
+
parse(value: unknown): T
|
|
31
|
+
/** Parse and validate a value, returning a result object. */
|
|
32
|
+
safeParse(value: unknown): { success: true; value: T } | { success: false; issues: Array<{ message: string }> }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createPreset<T>(vendor: string, validate: (value: unknown) => T): Preset<T> {
|
|
36
|
+
const schema: Preset<T> = {
|
|
37
|
+
"~standard": {
|
|
38
|
+
version: 1,
|
|
39
|
+
vendor,
|
|
40
|
+
validate: (value) => {
|
|
41
|
+
try {
|
|
42
|
+
return { value: validate(value) }
|
|
43
|
+
} catch (e: any) {
|
|
44
|
+
return { issues: [{ message: e.message }] }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
parse(value: unknown): T {
|
|
49
|
+
const result = schema["~standard"].validate(value)
|
|
50
|
+
if ("issues" in result) throw new Error(result.issues[0]?.message ?? "Validation failed")
|
|
51
|
+
return result.value
|
|
52
|
+
},
|
|
53
|
+
safeParse(value: unknown) {
|
|
54
|
+
const result = schema["~standard"].validate(value)
|
|
55
|
+
if ("issues" in result) return { success: false as const, issues: [...result.issues] }
|
|
56
|
+
return { success: true as const, value: result.value }
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
return schema
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const VENDOR = "@silvery/commander"
|
|
63
|
+
|
|
64
|
+
/** Integer (coerced from string). */
|
|
65
|
+
export const int = createPreset<number>(VENDOR, (v) => {
|
|
66
|
+
const s = String(v).trim()
|
|
67
|
+
if (s === "") throw new Error(`Expected integer, got "${v}"`)
|
|
68
|
+
const n = Number(s)
|
|
69
|
+
if (!Number.isInteger(n)) throw new Error(`Expected integer, got "${v}"`)
|
|
70
|
+
return n
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/** Unsigned integer (>= 0, coerced from string). */
|
|
74
|
+
export const uint = createPreset<number>(VENDOR, (v) => {
|
|
75
|
+
const s = String(v).trim()
|
|
76
|
+
if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
77
|
+
const n = Number(s)
|
|
78
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
79
|
+
return n
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/** Float (coerced from string). */
|
|
83
|
+
export const float = createPreset<number>(VENDOR, (v) => {
|
|
84
|
+
const s = String(v).trim()
|
|
85
|
+
if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`)
|
|
86
|
+
const n = Number(s)
|
|
87
|
+
if (Number.isNaN(n)) throw new Error(`Expected number, got "${v}"`)
|
|
88
|
+
return n
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** Port number (1-65535). */
|
|
92
|
+
export const port = createPreset<number>(VENDOR, (v) => {
|
|
93
|
+
const n = Number(v)
|
|
94
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`)
|
|
95
|
+
return n
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** URL (validated via URL constructor). */
|
|
99
|
+
export const url = createPreset<string>(VENDOR, (v) => {
|
|
100
|
+
const s = String(v)
|
|
101
|
+
try {
|
|
102
|
+
new URL(s)
|
|
103
|
+
return s
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error(`Expected valid URL, got "${v}"`)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
/** File path (non-empty string). */
|
|
110
|
+
export const path = createPreset<string>(VENDOR, (v) => {
|
|
111
|
+
const s = String(v)
|
|
112
|
+
if (!s) throw new Error("Expected non-empty path")
|
|
113
|
+
return s
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/** Comma-separated values to string[]. */
|
|
117
|
+
export const csv = createPreset<string[]>(VENDOR, (v) => {
|
|
118
|
+
return String(v)
|
|
119
|
+
.split(",")
|
|
120
|
+
.map((s) => s.trim())
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** JSON string to parsed value. */
|
|
125
|
+
export const json = createPreset<unknown>(VENDOR, (v) => {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(String(v))
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error(`Expected valid JSON, got "${v}"`)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
|
|
134
|
+
export const bool = createPreset<boolean>(VENDOR, (v) => {
|
|
135
|
+
const s = String(v).toLowerCase()
|
|
136
|
+
if (["true", "1", "yes", "y"].includes(s)) return true
|
|
137
|
+
if (["false", "0", "no", "n"].includes(s)) return false
|
|
138
|
+
throw new Error(`Expected boolean (true/false/yes/no/1/0), got "${v}"`)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
/** Date string to Date object. */
|
|
142
|
+
export const date = createPreset<Date>(VENDOR, (v) => {
|
|
143
|
+
const d = new Date(String(v))
|
|
144
|
+
if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`)
|
|
145
|
+
return d
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/** Email address (basic validation). */
|
|
149
|
+
export const email = createPreset<string>(VENDOR, (v) => {
|
|
150
|
+
const s = String(v)
|
|
151
|
+
if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`)
|
|
152
|
+
return s
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/** Regex pattern string to RegExp. */
|
|
156
|
+
export const regex = createPreset<RegExp>(VENDOR, (v) => {
|
|
157
|
+
try {
|
|
158
|
+
return new RegExp(String(v))
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`Expected valid regex, got "${v}"`)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/** Integer with min/max bounds (factory). */
|
|
165
|
+
export function intRange(min: number, max: number): Preset<number> {
|
|
166
|
+
return createPreset<number>(VENDOR, (v) => {
|
|
167
|
+
const n = Number(v)
|
|
168
|
+
if (!Number.isInteger(n) || n < min || n > max)
|
|
169
|
+
throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
170
|
+
return n
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Enum from a fixed set of string values (factory). */
|
|
175
|
+
export function oneOf<const T extends readonly string[]>(values: T): Preset<T[number]> {
|
|
176
|
+
return createPreset<T[number]>(VENDOR, (v) => {
|
|
177
|
+
const s = String(v)
|
|
178
|
+
if (!values.includes(s as any)) throw new Error(`Expected one of [${values.join(", ")}], got "${v}"`)
|
|
179
|
+
return s as T[number]
|
|
180
|
+
})
|
|
181
|
+
}
|
package/src/typed.ts
CHANGED
|
@@ -99,11 +99,50 @@ type ArgType<S extends string> = S extends `<${string}>`
|
|
|
99
99
|
/** Resolve accumulated option types for use in action handler signatures */
|
|
100
100
|
export type TypedOpts<Opts> = Prettify<Opts>
|
|
101
101
|
|
|
102
|
-
// ---
|
|
102
|
+
// --- Standard Schema support (v1) ---
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
105
|
+
* Standard Schema v1 interface — the universal schema interop protocol.
|
|
106
|
+
* Supports any schema library that implements Standard Schema (Zod >=3.24,
|
|
107
|
+
* Valibot >=1.0, ArkType >=2.0, etc.).
|
|
108
|
+
*
|
|
109
|
+
* Inlined to avoid any dependency on @standard-schema/spec.
|
|
110
|
+
* See: https://github.com/standard-schema/standard-schema
|
|
111
|
+
*/
|
|
112
|
+
export interface StandardSchemaV1<T = unknown> {
|
|
113
|
+
readonly "~standard": {
|
|
114
|
+
readonly version: 1
|
|
115
|
+
readonly vendor: string
|
|
116
|
+
readonly validate: (
|
|
117
|
+
value: unknown,
|
|
118
|
+
) => { value: T } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }> }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Type-level extraction: infer the output type from a Standard Schema */
|
|
123
|
+
type InferStandardSchema<S> = S extends StandardSchemaV1<infer T> ? T : never
|
|
124
|
+
|
|
125
|
+
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
126
|
+
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
127
|
+
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Wrap a Standard Schema as a Commander parser function */
|
|
131
|
+
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
132
|
+
return (value: string) => {
|
|
133
|
+
const result = schema["~standard"].validate(value)
|
|
134
|
+
if ("issues" in result) {
|
|
135
|
+
const msg = result.issues.map((i) => i.message).join(", ")
|
|
136
|
+
throw new Error(msg)
|
|
137
|
+
}
|
|
138
|
+
return result.value
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
107
146
|
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
108
147
|
*/
|
|
109
148
|
interface ZodLike<T = any> {
|
|
@@ -114,18 +153,19 @@ interface ZodLike<T = any> {
|
|
|
114
153
|
/** Type-level extraction: if Z is a Zod schema, infer its output type */
|
|
115
154
|
type InferZodOutput<Z> = Z extends { parse(value: unknown): infer T; _def: unknown } ? T : never
|
|
116
155
|
|
|
117
|
-
/** Runtime check: is this value a Zod-like schema? */
|
|
118
|
-
function
|
|
156
|
+
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
157
|
+
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
119
158
|
return (
|
|
120
159
|
typeof value === "object" &&
|
|
121
160
|
value !== null &&
|
|
122
161
|
typeof (value as any).parse === "function" &&
|
|
123
|
-
"_def" in (value as any)
|
|
162
|
+
"_def" in (value as any) &&
|
|
163
|
+
!("~standard" in (value as any))
|
|
124
164
|
)
|
|
125
165
|
}
|
|
126
166
|
|
|
127
|
-
/** Wrap a Zod schema as a Commander parser function */
|
|
128
|
-
function
|
|
167
|
+
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
168
|
+
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
129
169
|
return (value: string) => {
|
|
130
170
|
try {
|
|
131
171
|
return schema.parse(value)
|
|
@@ -173,12 +213,19 @@ export class TypedCommand<Opts = {}, Args extends any[] = []> {
|
|
|
173
213
|
/**
|
|
174
214
|
* Add an option with type inference.
|
|
175
215
|
*
|
|
176
|
-
* Supports
|
|
216
|
+
* Supports five overload patterns:
|
|
177
217
|
* 1. `.option(flags, description?)` — type inferred from flags syntax
|
|
178
218
|
* 2. `.option(flags, description, defaultValue)` — removes `undefined` from type
|
|
179
219
|
* 3. `.option(flags, description, parser, defaultValue?)` — type inferred from parser return type
|
|
180
|
-
* 4. `.option(flags, description,
|
|
220
|
+
* 4. `.option(flags, description, standardSchema)` — type inferred from Standard Schema output
|
|
221
|
+
* 5. `.option(flags, description, zodSchema)` — type inferred from Zod schema output (legacy, pre-3.24)
|
|
181
222
|
*/
|
|
223
|
+
option<const F extends string, S extends StandardSchemaV1>(
|
|
224
|
+
flags: F,
|
|
225
|
+
description: string,
|
|
226
|
+
schema: S,
|
|
227
|
+
): TypedCommand<Opts & { [K in ExtractLongName<F>]: InferStandardSchema<S> }, Args>
|
|
228
|
+
|
|
182
229
|
option<const F extends string, Z extends ZodLike>(
|
|
183
230
|
flags: F,
|
|
184
231
|
description: string,
|
|
@@ -199,8 +246,10 @@ export class TypedCommand<Opts = {}, Args extends any[] = []> {
|
|
|
199
246
|
): TypedCommand<AddOption<Opts, F, D>, Args>
|
|
200
247
|
|
|
201
248
|
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): any {
|
|
202
|
-
if (
|
|
203
|
-
;(this._cmd as any).option(flags, description ?? "",
|
|
249
|
+
if (isStandardSchema(parseArgOrDefault)) {
|
|
250
|
+
;(this._cmd as any).option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
251
|
+
} else if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
252
|
+
;(this._cmd as any).option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
204
253
|
} else if (typeof parseArgOrDefault === "function") {
|
|
205
254
|
;(this._cmd as any).option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
206
255
|
} else {
|