@silvery/commander 0.4.0 → 0.6.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 +46 -139
- package/package.json +1 -1
- package/src/command.ts +26 -22
- package/src/index.ts +3 -3
- package/src/presets.ts +23 -33
- package/src/z.ts +5 -6
package/README.md
CHANGED
|
@@ -1,169 +1,76 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe [Commander.js](https://github.com/tj/commander.js) with validated options, colorized help, and [Standard Schema](https://github.com/standard-schema/standard-schema) support. Drop-in replacement — `Command` extends Commander's `Command`. Install once, Commander is included.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// Layer 1: Enhanced Commander (auto-colorized help, Standard Schema support)
|
|
9
|
-
import { Command, port, csv } from "@silvery/commander"
|
|
10
|
-
|
|
11
|
-
// Layer 2: Zero-dep presets (Standard Schema, standalone use)
|
|
12
|
-
import { port, csv, int } from "@silvery/commander/parse"
|
|
13
|
-
|
|
14
|
-
// Layer 3: Zod + CLI presets (batteries included)
|
|
15
|
-
import { Command, z } from "@silvery/commander"
|
|
5
|
+
```bash
|
|
6
|
+
npm install @silvery/commander
|
|
16
7
|
```
|
|
17
8
|
|
|
18
|
-
##
|
|
9
|
+
## Example
|
|
19
10
|
|
|
20
11
|
```typescript
|
|
21
|
-
import { Command,
|
|
12
|
+
import { Command, z } from "@silvery/commander"
|
|
22
13
|
|
|
23
14
|
const program = new Command("deploy")
|
|
24
|
-
.description("Deploy
|
|
15
|
+
.description("Deploy to an environment")
|
|
25
16
|
.version("1.0.0")
|
|
26
|
-
.option("-
|
|
27
|
-
.option("--
|
|
28
|
-
.option("-
|
|
17
|
+
.option("-e, --env <env>", "Target environment", z.enum(["dev", "staging", "prod"]))
|
|
18
|
+
.option("-p, --port <n>", "Port number", z.port)
|
|
19
|
+
.option("-r, --retries <n>", "Retry count", z.int)
|
|
20
|
+
.option("--tags <t>", "Labels", z.csv)
|
|
21
|
+
.option("-f, --force", "Skip confirmation")
|
|
29
22
|
|
|
30
23
|
program.parse()
|
|
31
|
-
const
|
|
24
|
+
const { env, port, retries, tags, force } = program.opts()
|
|
25
|
+
// │ │ │ │ └─ boolean | undefined
|
|
26
|
+
// │ │ │ └──────── string[]
|
|
27
|
+
// │ │ └────────────────── number
|
|
28
|
+
// │ └──────────────────────── number (1–65535)
|
|
29
|
+
// └─────────────────────────────── "dev" | "staging" | "prod"
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
With plain Commander, `opts()` returns `Record<string, any>` — every value is untyped. With `@silvery/commander`, each option's type is inferred from its schema: `z.port` produces `number`, `z.enum(...)` produces a union, `z.csv` produces `string[]`. Invalid values are rejected at parse time with clear error messages — not silently passed through as strings.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
[Zod](https://github.com/colinhacks/zod) is entirely optional — `z` is tree-shaken from your bundle if you don't import it. Without Zod, use the built-in types (`port`, `int`, `csv`) or plain Commander.
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
import { Command } from "commander"
|
|
40
|
-
import { colorizeHelp } from "@silvery/commander"
|
|
36
|
+
<pre><code>$ deploy --help
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
colorizeHelp(program) // applies recursively to all subcommands
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Standard Schema validation
|
|
38
|
+
<b>Usage:</b> <span style="color:#56b6c2">deploy</span> <span style="color:#98c379">[options]</span>
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
Deploy to an environment
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
<b>Options:</b>
|
|
43
|
+
<span style="color:#98c379">-V, --version</span> <span style="color:#888">output the version number</span>
|
|
44
|
+
<span style="color:#98c379">-e, --env <env></span> <span style="color:#888">Target environment</span>
|
|
45
|
+
<span style="color:#98c379">-p, --port <n></span> <span style="color:#888">Port number</span>
|
|
46
|
+
<span style="color:#98c379">-r, --retries <n></span> <span style="color:#888">Retry count</span>
|
|
47
|
+
<span style="color:#98c379">--tags <t></span> <span style="color:#888">Labels</span>
|
|
48
|
+
<span style="color:#98c379">-f, --force</span> <span style="color:#888">Skip confirmation</span>
|
|
49
|
+
<span style="color:#98c379">-h, --help</span> <span style="color:#888">display help for command</span>
|
|
50
|
+
</code></pre>
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
56
|
-
.option("-e, --env <env>", "Env", z.enum(["dev", "staging", "prod"]))
|
|
57
|
-
.option(
|
|
58
|
-
"--tags <t>",
|
|
59
|
-
"Tags",
|
|
60
|
-
z.string().transform((v) => v.split(",")),
|
|
61
|
-
)
|
|
62
|
-
```
|
|
52
|
+
Help is auto-colorized — bold headings, green flags, cyan commands, dim descriptions. Options with [Zod](https://github.com/colinhacks/zod) schemas or built-in types are validated at parse time with clear error messages.
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
## What's included
|
|
65
55
|
|
|
66
|
-
|
|
56
|
+
- **Colorized help** — automatic, with color level detection and [`NO_COLOR`](https://no-color.org)/`FORCE_COLOR` support via [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) (optional)
|
|
57
|
+
- **Typed `.option()` parsing** — pass a type as the third argument:
|
|
58
|
+
- 14 built-in types — `port`, `int`, `csv`, `url`, `email`, `date`, [more](https://silvery.dev/reference/commander)
|
|
59
|
+
- Array choices — `["dev", "staging", "prod"]`
|
|
60
|
+
- [Zod](https://github.com/colinhacks/zod) schemas — `z.port`, `z.int`, `z.csv`, or any custom `z.string()`, `z.number()`, etc.
|
|
61
|
+
- Any [Standard Schema](https://github.com/standard-schema/standard-schema) library — [Valibot](https://github.com/fabian-hiller/valibot), [ArkType](https://github.com/arktypeio/arktype)
|
|
62
|
+
- All types usable standalone via `.parse()`/`.safeParse()`
|
|
67
63
|
|
|
68
|
-
|
|
64
|
+
## Docs
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
import { Command, z } from "@silvery/commander"
|
|
72
|
-
|
|
73
|
-
const program = new Command("deploy")
|
|
74
|
-
.option("-p, --port <n>", "Port", z.port) // z.coerce.number().int().min(1).max(65535)
|
|
75
|
-
.option("--tags <t>", "Tags", z.csv) // z.string().transform(...)
|
|
76
|
-
.option("-e, --env <e>", "Env", z.oneOf(["dev", "staging", "prod"]))
|
|
77
|
-
.option("-r, --retries <n>", "Retries", z.int) // z.coerce.number().int()
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
The `z` export is tree-shakeable -- if you don't import it, Zod won't be in your bundle.
|
|
81
|
-
|
|
82
|
-
Available `z` CLI presets: `z.port`, `z.int`, `z.uint`, `z.float`, `z.csv`, `z.url`, `z.path`, `z.email`, `z.date`, `z.json`, `z.bool`, `z.intRange(min, max)`, `z.oneOf(values)`.
|
|
83
|
-
|
|
84
|
-
## Presets
|
|
85
|
-
|
|
86
|
-
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.
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
import { Command, port, csv, int, url, oneOf } from "@silvery/commander"
|
|
90
|
-
|
|
91
|
-
const program = new Command("deploy")
|
|
92
|
-
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
93
|
-
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
94
|
-
.option("--tags <t>", "Tags", csv) // string[]
|
|
95
|
-
.option("--callback <url>", "Callback", url) // string (validated URL)
|
|
96
|
-
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Standalone usage
|
|
100
|
-
|
|
101
|
-
Presets also work outside Commander for validating env vars, config files, etc. Import from the `@silvery/commander/parse` subpath for tree-shaking:
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
import { port, csv, oneOf } from "@silvery/commander/parse"
|
|
105
|
-
|
|
106
|
-
// .parse() — returns value or throws
|
|
107
|
-
const dbPort = port.parse(process.env.DB_PORT ?? "5432") // 3000
|
|
108
|
-
|
|
109
|
-
// .safeParse() — returns result object, never throws
|
|
110
|
-
const result = port.safeParse("abc")
|
|
111
|
-
// { success: false, issues: [{ message: 'Expected port (1-65535), got "abc"' }] }
|
|
112
|
-
|
|
113
|
-
// Standard Schema ~standard.validate() also available
|
|
114
|
-
const validated = port["~standard"].validate("8080")
|
|
115
|
-
// { value: 8080 }
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Available presets
|
|
119
|
-
|
|
120
|
-
| Preset | Type | Validation |
|
|
121
|
-
| ------- | ---------- | ---------------------------------------- |
|
|
122
|
-
| `int` | `number` | Integer (coerced from string) |
|
|
123
|
-
| `uint` | `number` | Unsigned integer (>= 0) |
|
|
124
|
-
| `float` | `number` | Any finite number (rejects NaN) |
|
|
125
|
-
| `port` | `number` | Integer 1-65535 |
|
|
126
|
-
| `url` | `string` | Valid URL (via `URL` constructor) |
|
|
127
|
-
| `path` | `string` | Non-empty string |
|
|
128
|
-
| `csv` | `string[]` | Comma-separated, trimmed, empty filtered |
|
|
129
|
-
| `json` | `unknown` | Parsed JSON |
|
|
130
|
-
| `bool` | `boolean` | true/false/yes/no/1/0 (case-insensitive) |
|
|
131
|
-
| `date` | `Date` | Valid date string |
|
|
132
|
-
| `email` | `string` | Basic email validation (has @ and .) |
|
|
133
|
-
| `regex` | `RegExp` | Valid regex pattern |
|
|
134
|
-
|
|
135
|
-
### Factory presets
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
import { intRange, oneOf } from "@silvery/commander"
|
|
139
|
-
|
|
140
|
-
intRange(1, 100) // Preset<number> — integer within bounds
|
|
141
|
-
oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Custom parser type inference
|
|
145
|
-
|
|
146
|
-
When `.option()` is called with a parser function as the third argument, Commander infers the return type:
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
const program = new Command("deploy")
|
|
150
|
-
.option("-p, --port <n>", "Port", parseInt) // port: number
|
|
151
|
-
.option("-t, --timeout <ms>", "Timeout", Number) // timeout: number
|
|
152
|
-
.option("--tags <items>", "Tags", (v) => v.split(",")) // tags: string[]
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Default values can be passed as the fourth argument:
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
.option("-p, --port <n>", "Port", parseInt, 8080) // port: number (defaults to 8080)
|
|
159
|
-
```
|
|
66
|
+
Full reference, type table, and API details at **[silvery.dev/reference/commander](https://silvery.dev/reference/commander)**.
|
|
160
67
|
|
|
161
68
|
## Credits
|
|
162
69
|
|
|
163
|
-
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors
|
|
164
|
-
- [
|
|
165
|
-
- [
|
|
166
|
-
-
|
|
70
|
+
- **[Commander.js](https://github.com/tj/commander.js)** by TJ Holowaychuk and contributors
|
|
71
|
+
- **[@commander-js/extra-typings](https://github.com/commander-js/extra-typings)** — inspired the type inference approach
|
|
72
|
+
- **[Standard Schema](https://github.com/standard-schema/standard-schema)** — universal schema interop protocol
|
|
73
|
+
- **[@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi)** — terminal capability detection
|
|
167
74
|
|
|
168
75
|
## License
|
|
169
76
|
|
package/package.json
CHANGED
package/src/command.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enhanced Commander Command with auto-colorized help
|
|
2
|
+
* Enhanced Commander Command with auto-colorized help, Standard Schema support,
|
|
3
|
+
* and array-as-choices detection.
|
|
3
4
|
*
|
|
4
|
-
* Subclasses Commander's Command so `new Command("app")` just works
|
|
5
|
-
* it's Commander with auto-colorized help
|
|
6
|
-
* legacy Zod detection in `.option()`.
|
|
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()`.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```ts
|
|
10
|
-
* import { Command } from "@silvery/commander"
|
|
11
|
-
* import { port, csv } from "@silvery/commander/parse"
|
|
11
|
+
* import { Command, port, csv } from "@silvery/commander"
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* .description("My CLI tool")
|
|
15
|
-
* .version("1.0.0")
|
|
13
|
+
* new Command("deploy")
|
|
16
14
|
* .option("-p, --port <n>", "Port", port)
|
|
17
15
|
* .option("--tags <t>", "Tags", csv)
|
|
16
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
18
17
|
*
|
|
19
18
|
* program.parse()
|
|
20
19
|
* ```
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
|
-
import { Command as BaseCommand } from "commander"
|
|
22
|
+
import { Command as BaseCommand, Option } from "commander"
|
|
24
23
|
import { colorizeHelp } from "./colorize.ts"
|
|
25
24
|
import type { StandardSchemaV1 } from "./presets.ts"
|
|
26
25
|
|
|
@@ -88,21 +87,26 @@ export class Command extends BaseCommand {
|
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
/**
|
|
91
|
-
* Add an option with
|
|
90
|
+
* Add an option with smart third-argument detection.
|
|
92
91
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
92
|
+
* The third argument is detected in order:
|
|
93
|
+
* 1. **Array** -- treated as choices (Commander `.choices()`)
|
|
94
|
+
* 2. **Standard Schema v1** -- wrapped as a parser function
|
|
95
|
+
* 3. **Legacy Zod** (pre-3.24, has `_def` + `parse`) -- wrapped as a parser
|
|
96
|
+
* 4. **Function** -- passed through as Commander's parser function
|
|
97
|
+
* 5. **Anything else** -- passed through as a default value
|
|
99
98
|
*/
|
|
100
|
-
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
99
|
+
override option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
100
|
+
if (Array.isArray(parseArgOrDefault)) {
|
|
101
|
+
const opt = new Option(flags, description ?? "").choices(parseArgOrDefault as string[])
|
|
102
|
+
this.addOption(opt)
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
101
105
|
if (isStandardSchema(parseArgOrDefault)) {
|
|
102
|
-
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
106
|
+
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue)
|
|
103
107
|
}
|
|
104
108
|
if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
105
|
-
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
109
|
+
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue)
|
|
106
110
|
}
|
|
107
111
|
if (typeof parseArgOrDefault === "function") {
|
|
108
112
|
return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
@@ -110,8 +114,8 @@ export class Command extends BaseCommand {
|
|
|
110
114
|
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
// Subcommands also get colorized help
|
|
114
|
-
createCommand(name?: string): Command {
|
|
117
|
+
// Subcommands also get colorized help, Standard Schema, and array choices
|
|
118
|
+
override createCommand(name?: string): Command {
|
|
115
119
|
return new Command(name)
|
|
116
120
|
}
|
|
117
121
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLik
|
|
|
6
6
|
export { Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
|
|
7
7
|
export type { OptionValues } from "commander"
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange
|
|
11
|
-
export type {
|
|
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
12
|
|
|
13
13
|
// Tree-shakeable: only evaluated if user imports z
|
|
14
14
|
export { z } from "./z.ts"
|
package/src/presets.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Built-in CLI types — Standard Schema v1 validators for common CLI argument patterns.
|
|
3
3
|
*
|
|
4
4
|
* Zero dependencies — validation is manual, no Zod/Valibot/ArkType required.
|
|
5
|
-
* Each
|
|
5
|
+
* Each type implements Standard Schema v1 for interop with any schema library,
|
|
6
6
|
* plus standalone `.parse()` and `.safeParse()` convenience methods.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
10
|
-
* import {
|
|
10
|
+
* import { Command, port, csv } from "@silvery/commander"
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* new Command("deploy")
|
|
13
13
|
* .option("-p, --port <n>", "Port", port) // number (1-65535)
|
|
14
14
|
* .option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
15
15
|
* .option("--tags <t>", "Tags", csv) // string[]
|
|
16
|
-
* .option("--
|
|
17
|
-
* .option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
16
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
18
17
|
*
|
|
19
18
|
* // Standalone usage (outside Commander)
|
|
20
19
|
* port.parse("3000") // 3000
|
|
@@ -40,16 +39,16 @@ export interface StandardSchemaV1<T = unknown> {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
/** A Standard Schema v1
|
|
44
|
-
export interface
|
|
42
|
+
/** A Standard Schema v1 CLI type with standalone parse/safeParse methods. */
|
|
43
|
+
export interface CLIType<T> extends StandardSchemaV1<T> {
|
|
45
44
|
/** Parse and validate a value, throwing on failure. */
|
|
46
45
|
parse(value: unknown): T
|
|
47
46
|
/** Parse and validate a value, returning a result object. */
|
|
48
47
|
safeParse(value: unknown): { success: true; value: T } | { success: false; issues: Array<{ message: string }> }
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
function
|
|
52
|
-
const schema:
|
|
50
|
+
function createType<T>(vendor: string, validate: (value: unknown) => T): CLIType<T> {
|
|
51
|
+
const schema: CLIType<T> = {
|
|
53
52
|
"~standard": {
|
|
54
53
|
version: 1,
|
|
55
54
|
vendor,
|
|
@@ -78,7 +77,7 @@ function createPreset<T>(vendor: string, validate: (value: unknown) => T): Prese
|
|
|
78
77
|
const VENDOR = "@silvery/commander"
|
|
79
78
|
|
|
80
79
|
/** Integer (coerced from string). */
|
|
81
|
-
export const int =
|
|
80
|
+
export const int = createType<number>(VENDOR, (v) => {
|
|
82
81
|
const s = String(v).trim()
|
|
83
82
|
if (s === "") throw new Error(`Expected integer, got "${v}"`)
|
|
84
83
|
const n = Number(s)
|
|
@@ -87,7 +86,7 @@ export const int = createPreset<number>(VENDOR, (v) => {
|
|
|
87
86
|
})
|
|
88
87
|
|
|
89
88
|
/** Unsigned integer (>= 0, coerced from string). */
|
|
90
|
-
export const uint =
|
|
89
|
+
export const uint = createType<number>(VENDOR, (v) => {
|
|
91
90
|
const s = String(v).trim()
|
|
92
91
|
if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`)
|
|
93
92
|
const n = Number(s)
|
|
@@ -96,7 +95,7 @@ export const uint = createPreset<number>(VENDOR, (v) => {
|
|
|
96
95
|
})
|
|
97
96
|
|
|
98
97
|
/** Float (coerced from string). */
|
|
99
|
-
export const float =
|
|
98
|
+
export const float = createType<number>(VENDOR, (v) => {
|
|
100
99
|
const s = String(v).trim()
|
|
101
100
|
if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`)
|
|
102
101
|
const n = Number(s)
|
|
@@ -105,14 +104,14 @@ export const float = createPreset<number>(VENDOR, (v) => {
|
|
|
105
104
|
})
|
|
106
105
|
|
|
107
106
|
/** Port number (1-65535). */
|
|
108
|
-
export const port =
|
|
107
|
+
export const port = createType<number>(VENDOR, (v) => {
|
|
109
108
|
const n = Number(v)
|
|
110
109
|
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`)
|
|
111
110
|
return n
|
|
112
111
|
})
|
|
113
112
|
|
|
114
113
|
/** URL (validated via URL constructor). */
|
|
115
|
-
export const url =
|
|
114
|
+
export const url = createType<string>(VENDOR, (v) => {
|
|
116
115
|
const s = String(v)
|
|
117
116
|
try {
|
|
118
117
|
new URL(s)
|
|
@@ -123,14 +122,14 @@ export const url = createPreset<string>(VENDOR, (v) => {
|
|
|
123
122
|
})
|
|
124
123
|
|
|
125
124
|
/** File path (non-empty string). */
|
|
126
|
-
export const path =
|
|
125
|
+
export const path = createType<string>(VENDOR, (v) => {
|
|
127
126
|
const s = String(v)
|
|
128
127
|
if (!s) throw new Error("Expected non-empty path")
|
|
129
128
|
return s
|
|
130
129
|
})
|
|
131
130
|
|
|
132
131
|
/** Comma-separated values to string[]. */
|
|
133
|
-
export const csv =
|
|
132
|
+
export const csv = createType<string[]>(VENDOR, (v) => {
|
|
134
133
|
return String(v)
|
|
135
134
|
.split(",")
|
|
136
135
|
.map((s) => s.trim())
|
|
@@ -138,7 +137,7 @@ export const csv = createPreset<string[]>(VENDOR, (v) => {
|
|
|
138
137
|
})
|
|
139
138
|
|
|
140
139
|
/** JSON string to parsed value. */
|
|
141
|
-
export const json =
|
|
140
|
+
export const json = createType<unknown>(VENDOR, (v) => {
|
|
142
141
|
try {
|
|
143
142
|
return JSON.parse(String(v))
|
|
144
143
|
} catch {
|
|
@@ -147,7 +146,7 @@ export const json = createPreset<unknown>(VENDOR, (v) => {
|
|
|
147
146
|
})
|
|
148
147
|
|
|
149
148
|
/** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
|
|
150
|
-
export const bool =
|
|
149
|
+
export const bool = createType<boolean>(VENDOR, (v) => {
|
|
151
150
|
const s = String(v).toLowerCase()
|
|
152
151
|
if (["true", "1", "yes", "y"].includes(s)) return true
|
|
153
152
|
if (["false", "0", "no", "n"].includes(s)) return false
|
|
@@ -155,21 +154,21 @@ export const bool = createPreset<boolean>(VENDOR, (v) => {
|
|
|
155
154
|
})
|
|
156
155
|
|
|
157
156
|
/** Date string to Date object. */
|
|
158
|
-
export const date =
|
|
157
|
+
export const date = createType<Date>(VENDOR, (v) => {
|
|
159
158
|
const d = new Date(String(v))
|
|
160
159
|
if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`)
|
|
161
160
|
return d
|
|
162
161
|
})
|
|
163
162
|
|
|
164
163
|
/** Email address (basic validation). */
|
|
165
|
-
export const email =
|
|
164
|
+
export const email = createType<string>(VENDOR, (v) => {
|
|
166
165
|
const s = String(v)
|
|
167
166
|
if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`)
|
|
168
167
|
return s
|
|
169
168
|
})
|
|
170
169
|
|
|
171
170
|
/** Regex pattern string to RegExp. */
|
|
172
|
-
export const regex =
|
|
171
|
+
export const regex = createType<RegExp>(VENDOR, (v) => {
|
|
173
172
|
try {
|
|
174
173
|
return new RegExp(String(v))
|
|
175
174
|
} catch {
|
|
@@ -178,19 +177,10 @@ export const regex = createPreset<RegExp>(VENDOR, (v) => {
|
|
|
178
177
|
})
|
|
179
178
|
|
|
180
179
|
/** Integer with min/max bounds (factory). */
|
|
181
|
-
export function intRange(min: number, max: number):
|
|
182
|
-
return
|
|
180
|
+
export function intRange(min: number, max: number): CLIType<number> {
|
|
181
|
+
return createType<number>(VENDOR, (v) => {
|
|
183
182
|
const n = Number(v)
|
|
184
183
|
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
185
184
|
return n
|
|
186
185
|
})
|
|
187
186
|
}
|
|
188
|
-
|
|
189
|
-
/** Enum from a fixed set of string values (factory). */
|
|
190
|
-
export function oneOf<const T extends readonly string[]>(values: T): Preset<T[number]> {
|
|
191
|
-
return createPreset<T[number]>(VENDOR, (v) => {
|
|
192
|
-
const s = String(v)
|
|
193
|
-
if (!values.includes(s as any)) throw new Error(`Expected one of [${values.join(", ")}], got "${v}"`)
|
|
194
|
-
return s as T[number]
|
|
195
|
-
})
|
|
196
|
-
}
|
package/src/z.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extended Zod object with CLI
|
|
2
|
+
* Extended Zod object with CLI types.
|
|
3
3
|
*
|
|
4
4
|
* Spreads all of Zod's exports and adds CLI-specific schemas.
|
|
5
|
-
* Tree-shakeable
|
|
5
|
+
* Tree-shakeable -- only evaluated if user imports `z`.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
9
|
* import { z, Command } from "@silvery/commander"
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* new Command("deploy")
|
|
12
12
|
* .option("-p, --port <n>", "Port", z.port)
|
|
13
|
-
* .option("-e, --env <e>", "Environment", z.oneOf(["dev", "staging", "prod"]))
|
|
14
13
|
* .option("--tags <t>", "Tags", z.csv)
|
|
14
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
15
15
|
*
|
|
16
16
|
* program.parse()
|
|
17
17
|
* ```
|
|
@@ -21,7 +21,7 @@ import * as zod from "zod"
|
|
|
21
21
|
|
|
22
22
|
export const z = {
|
|
23
23
|
...zod,
|
|
24
|
-
// CLI
|
|
24
|
+
// CLI types (built on Zod schemas)
|
|
25
25
|
port: zod.coerce.number().int().min(1).max(65535),
|
|
26
26
|
int: zod.coerce.number().int(),
|
|
27
27
|
uint: zod.coerce.number().int().min(0),
|
|
@@ -41,5 +41,4 @@ export const z = {
|
|
|
41
41
|
.enum(["true", "false", "1", "0", "yes", "no", "y", "n"] as const)
|
|
42
42
|
.transform((v: string) => ["true", "1", "yes", "y"].includes(v)),
|
|
43
43
|
intRange: (min: number, max: number) => zod.coerce.number().int().min(min).max(max),
|
|
44
|
-
oneOf: <const T extends readonly [string, ...string[]]>(values: T) => zod.enum(values),
|
|
45
44
|
}
|