@silvery/commander 0.4.0 → 0.5.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 +108 -99
- package/package.json +1 -1
- package/src/command.ts +22 -18
- package/src/index.ts +3 -3
- package/src/presets.ts +23 -33
- package/src/z.ts +5 -6
package/README.md
CHANGED
|
@@ -1,57 +1,103 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
Enhanced [Commander.js](https://github.com/tj/commander.js) with auto-colorized help, Standard Schema validation, and
|
|
3
|
+
Enhanced [Commander.js](https://github.com/tj/commander.js) with type-safe options, auto-colorized help, [Standard Schema](https://github.com/standard-schema/standard-schema) validation, and built-in CLI types.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Drop-in replacement -- `Command` is a subclass of Commander's `Command` with full type inference for options, arguments, and parsed values. Install once, Commander is included.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
8
|
-
// Layer 1: Enhanced Commander (auto-colorized help, Standard Schema support)
|
|
9
10
|
import { Command, port, csv } from "@silvery/commander"
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
new Command("deploy")
|
|
13
|
+
.description("Deploy the application")
|
|
14
|
+
.version("1.0.0")
|
|
15
|
+
.option("-p, --port <n>", "Port", port)
|
|
16
|
+
.option("--tags <t>", "Tags", csv)
|
|
17
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
import { Command, z } from "@silvery/commander"
|
|
19
|
+
program.parse()
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
Help output is automatically colorized -- bold headings, green flags, cyan commands, dim descriptions, yellow arguments. Uses [Commander's](https://github.com/tj/commander.js) built-in `configureHelp()` style hooks.
|
|
23
|
+
|
|
24
|
+
Colorization works out of the box with raw ANSI codes. Install [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) for full terminal capability detection (respects `NO_COLOR`, `FORCE_COLOR`, and `isTTY`).
|
|
25
|
+
|
|
26
|
+
## Validated options with built-in types
|
|
27
|
+
|
|
28
|
+
Commander's `.option()` accepts a string and gives you a string back. Our built-in types parse and validate in one step:
|
|
19
29
|
|
|
20
30
|
```typescript
|
|
21
|
-
import { Command, port, csv,
|
|
31
|
+
import { Command, port, csv, int } from "@silvery/commander"
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
.version("1.0.0")
|
|
26
|
-
.option("-p, --port <n>", "Port", port) // number (1-65535)
|
|
33
|
+
new Command("deploy")
|
|
34
|
+
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
27
35
|
.option("--tags <t>", "Tags", csv) // string[]
|
|
28
|
-
.option("-
|
|
36
|
+
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
37
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"]) // choices
|
|
38
|
+
```
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
These types are **not part of Commander** -- they're provided by `@silvery/commander`. Each implements [Standard Schema v1](https://github.com/standard-schema/standard-schema), so they work with any schema-aware tooling. They have zero dependencies.
|
|
41
|
+
|
|
42
|
+
### Available types
|
|
43
|
+
|
|
44
|
+
| Type | Output | Validation |
|
|
45
|
+
| ------- | ---------- | ---------------------------------------- |
|
|
46
|
+
| `int` | `number` | Integer (coerced from string) |
|
|
47
|
+
| `uint` | `number` | Unsigned integer (>= 0) |
|
|
48
|
+
| `float` | `number` | Any finite number (rejects NaN) |
|
|
49
|
+
| `port` | `number` | Integer 1-65535 |
|
|
50
|
+
| `url` | `string` | Valid URL (via `URL` constructor) |
|
|
51
|
+
| `path` | `string` | Non-empty string |
|
|
52
|
+
| `csv` | `string[]` | Comma-separated, trimmed, empty filtered |
|
|
53
|
+
| `json` | `unknown` | Parsed JSON |
|
|
54
|
+
| `bool` | `boolean` | true/false/yes/no/1/0 (case-insensitive) |
|
|
55
|
+
| `date` | `Date` | Valid date string |
|
|
56
|
+
| `email` | `string` | Basic email validation |
|
|
57
|
+
| `regex` | `RegExp` | Valid regex pattern |
|
|
58
|
+
|
|
59
|
+
### Factory type
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { intRange } from "@silvery/commander"
|
|
63
|
+
|
|
64
|
+
intRange(1, 100) // CLIType<number> -- integer within bounds
|
|
32
65
|
```
|
|
33
66
|
|
|
34
|
-
|
|
67
|
+
### Array choices
|
|
35
68
|
|
|
36
|
-
|
|
69
|
+
Pass an array as the third argument to restrict an option to a fixed set of values:
|
|
37
70
|
|
|
38
71
|
```typescript
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
73
|
+
```
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
Commander validates the choice at parse time and rejects invalid values.
|
|
76
|
+
|
|
77
|
+
### Standalone usage
|
|
78
|
+
|
|
79
|
+
Types work outside Commander too -- for validating env vars, config files, etc.:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { port, csv } from "@silvery/commander/parse"
|
|
83
|
+
|
|
84
|
+
port.parse("3000") // 3000
|
|
85
|
+
port.parse("abc") // throws: 'Expected port (1-65535), got "abc"'
|
|
86
|
+
port.safeParse("3000") // { success: true, value: 3000 }
|
|
87
|
+
port.safeParse("abc") // { success: false, issues: [{ message: "..." }] }
|
|
44
88
|
```
|
|
45
89
|
|
|
90
|
+
The `/parse` subpath has zero dependencies -- no Commander, no [Zod](https://github.com/colinhacks/zod).
|
|
91
|
+
|
|
46
92
|
## Standard Schema validation
|
|
47
93
|
|
|
48
|
-
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema)
|
|
94
|
+
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) schema as the third argument to `.option()`. This works with [Zod](https://github.com/colinhacks/zod) (>=3.24), [Valibot](https://github.com/fabian-hiller/valibot) (>=1.0), [ArkType](https://github.com/arktypeio/arktype) (>=2.0), and any library implementing the protocol:
|
|
49
95
|
|
|
50
96
|
```typescript
|
|
51
97
|
import { Command } from "@silvery/commander"
|
|
52
98
|
import { z } from "zod"
|
|
53
99
|
|
|
54
|
-
|
|
100
|
+
new Command("deploy")
|
|
55
101
|
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
56
102
|
.option("-e, --env <env>", "Env", z.enum(["dev", "staging", "prod"]))
|
|
57
103
|
.option(
|
|
@@ -61,109 +107,72 @@ const program = new Command("deploy")
|
|
|
61
107
|
)
|
|
62
108
|
```
|
|
63
109
|
|
|
64
|
-
Schema libraries are optional peer dependencies -- detected at runtime
|
|
110
|
+
Schema libraries are optional peer dependencies -- detected at runtime, never imported at the top level.
|
|
65
111
|
|
|
66
|
-
## Zod CLI
|
|
112
|
+
## Zod CLI types
|
|
67
113
|
|
|
68
|
-
Import `z` from `@silvery/commander` for
|
|
114
|
+
Import `z` from `@silvery/commander` for [Zod](https://github.com/colinhacks/zod) extended with CLI-specific schemas:
|
|
69
115
|
|
|
70
116
|
```typescript
|
|
71
117
|
import { Command, z } from "@silvery/commander"
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
.option("-p, --port <n>", "Port", z.port)
|
|
75
|
-
.option("--tags <t>", "Tags", z.csv)
|
|
76
|
-
.option("-
|
|
77
|
-
.option("-
|
|
119
|
+
new Command("deploy")
|
|
120
|
+
.option("-p, --port <n>", "Port", z.port)
|
|
121
|
+
.option("--tags <t>", "Tags", z.csv)
|
|
122
|
+
.option("-r, --retries <n>", "Retries", z.int)
|
|
123
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
78
124
|
```
|
|
79
125
|
|
|
80
|
-
The `z` export is tree-shakeable -- if you don't import it, Zod won't be in your bundle.
|
|
126
|
+
The `z` export is tree-shakeable -- if you don't import it, [Zod](https://github.com/colinhacks/zod) won't be in your bundle. Requires `zod` as a peer dependency.
|
|
81
127
|
|
|
82
|
-
Available
|
|
128
|
+
Available: `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)`.
|
|
83
129
|
|
|
84
|
-
##
|
|
130
|
+
## Function parsers
|
|
85
131
|
|
|
86
|
-
|
|
132
|
+
[Commander's](https://github.com/tj/commander.js) standard parser function pattern also works:
|
|
87
133
|
|
|
88
134
|
```typescript
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.option("-p, --port <n>", "Port",
|
|
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"]))
|
|
135
|
+
new Command("app")
|
|
136
|
+
.option("-p, --port <n>", "Port", parseInt) // number
|
|
137
|
+
.option("--tags <items>", "Tags", (v) => v.split(",")) // string[]
|
|
138
|
+
.option("-p, --port <n>", "Port", parseInt, 8080) // number (with default)
|
|
97
139
|
```
|
|
98
140
|
|
|
99
|
-
|
|
141
|
+
## colorizeHelp()
|
|
100
142
|
|
|
101
|
-
|
|
143
|
+
Use standalone with a plain [Commander](https://github.com/tj/commander.js) `Command` (without subclassing):
|
|
102
144
|
|
|
103
145
|
```typescript
|
|
104
|
-
import {
|
|
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"
|
|
146
|
+
import { Command } from "commander"
|
|
147
|
+
import { colorizeHelp } from "@silvery/commander"
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const program = new Command("myapp")
|
|
150
|
+
colorizeHelp(program) // applies recursively to all subcommands
|
|
142
151
|
```
|
|
143
152
|
|
|
144
|
-
##
|
|
153
|
+
## Import paths
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
| Path | What | Dependencies |
|
|
156
|
+
| -------------------------- | ------------------------------- | ----------------------------------------------- |
|
|
157
|
+
| `@silvery/commander` | Command, colorizeHelp, types, z | [commander](https://github.com/tj/commander.js) |
|
|
158
|
+
| `@silvery/commander/parse` | Types only (.parse/.safeParse) | none |
|
|
147
159
|
|
|
148
|
-
|
|
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
|
-
```
|
|
160
|
+
## Beyond extra-typings
|
|
154
161
|
|
|
155
|
-
|
|
162
|
+
Built on the shoulders of [@commander-js/extra-typings](https://github.com/commander-js/extra-typings). We add:
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
- **Auto-colorized help** -- bold headings, green flags, cyan commands
|
|
165
|
+
- **Built-in validation** via [Standard Schema](https://github.com/standard-schema/standard-schema) -- works with [Zod](https://github.com/colinhacks/zod), [Valibot](https://github.com/fabian-hiller/valibot), [ArkType](https://github.com/arktypeio/arktype)
|
|
166
|
+
- **14 CLI types** -- `port`, `csv`, `int`, `url`, `email` and more, usable standalone via `.parse()`/`.safeParse()`
|
|
167
|
+
- **NO_COLOR support** via [`@silvery/ansi`](https://github.com/beorn/silvery/tree/main/packages/ansi) (optional)
|
|
168
|
+
- **Commander included** -- one install, no peer dep setup
|
|
160
169
|
|
|
161
170
|
## Credits
|
|
162
171
|
|
|
163
|
-
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
164
|
-
- [
|
|
165
|
-
- [
|
|
166
|
-
-
|
|
172
|
+
- **[Commander.js](https://github.com/tj/commander.js)** by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
173
|
+
- **[@commander-js/extra-typings](https://github.com/commander-js/extra-typings)** -- inspired the type inference approach
|
|
174
|
+
- **[Standard Schema](https://github.com/standard-schema/standard-schema)** -- universal schema interop protocol
|
|
175
|
+
- **[@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi)** -- optional terminal capability detection
|
|
167
176
|
|
|
168
177
|
## License
|
|
169
178
|
|
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,16 +87,21 @@ 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
99
|
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
100
|
+
if (Array.isArray(parseArgOrDefault)) {
|
|
101
|
+
const opt = new Option(flags, description ?? "").choices(parseArgOrDefault)
|
|
102
|
+
this.addOption(opt)
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
101
105
|
if (isStandardSchema(parseArgOrDefault)) {
|
|
102
106
|
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
103
107
|
}
|
|
@@ -110,7 +114,7 @@ export class Command extends BaseCommand {
|
|
|
110
114
|
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
// Subcommands also get colorized help
|
|
117
|
+
// Subcommands also get colorized help, Standard Schema, and array choices
|
|
114
118
|
createCommand(name?: string): Command {
|
|
115
119
|
return new Command(name)
|
|
116
120
|
}
|
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
|
}
|