@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 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 CLI presets. Drop-in replacement -- `Command` is a subclass of Commander's `Command`.
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
- ## Three layers
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
- // Layer 2: Zero-dep presets (Standard Schema, standalone use)
12
- import { port, csv, int } from "@silvery/commander/parse"
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
- // Layer 3: Zod + CLI presets (batteries included)
15
- import { Command, z } from "@silvery/commander"
19
+ program.parse()
16
20
  ```
17
21
 
18
- ## Usage
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, oneOf } from "@silvery/commander"
31
+ import { Command, port, csv, int } from "@silvery/commander"
22
32
 
23
- const program = new Command("deploy")
24
- .description("Deploy the application")
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("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
36
+ .option("-r, --retries <n>", "Retries", int) // number (integer)
37
+ .option("-e, --env <e>", "Env", ["dev", "staging", "prod"]) // choices
38
+ ```
29
39
 
30
- program.parse()
31
- const opts = program.opts()
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
- Help output is automatically colorized using Commander's built-in `configureHelp()` style hooks (headings bold, flags green, commands cyan, descriptions dim, arguments yellow).
67
+ ### Array choices
35
68
 
36
- You can also use `colorizeHelp()` standalone with a plain Commander `Command`:
69
+ Pass an array as the third argument to restrict an option to a fixed set of values:
37
70
 
38
71
  ```typescript
39
- import { Command } from "commander"
40
- import { colorizeHelp } from "@silvery/commander"
72
+ .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
73
+ ```
41
74
 
42
- const program = new Command("myapp").description("My CLI tool")
43
- colorizeHelp(program) // applies recursively to all subcommands
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) compatible schema as the third argument to `.option()` for combined parsing, validation, and type inference. This works with the built-in presets, Zod (>=3.24), Valibot (>=1.0), ArkType (>=2.0), and any other library implementing the standard:
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
- const program = new Command("deploy")
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 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.
110
+ Schema libraries are optional peer dependencies -- detected at runtime, never imported at the top level.
65
111
 
66
- ## Zod CLI presets
112
+ ## Zod CLI types
67
113
 
68
- Import `z` from `@silvery/commander` for an extended Zod object with CLI-specific schemas:
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
- 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()
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 `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)`.
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
- ## Presets
130
+ ## Function parsers
85
131
 
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.
132
+ [Commander's](https://github.com/tj/commander.js) standard parser function pattern also works:
87
133
 
88
134
  ```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"]))
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
- ### Standalone usage
141
+ ## colorizeHelp()
100
142
 
101
- Presets also work outside Commander for validating env vars, config files, etc. Import from the `@silvery/commander/parse` subpath for tree-shaking:
143
+ Use standalone with a plain [Commander](https://github.com/tj/commander.js) `Command` (without subclassing):
102
144
 
103
145
  ```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"
146
+ import { Command } from "commander"
147
+ import { colorizeHelp } from "@silvery/commander"
139
148
 
140
- intRange(1, 100) // Preset<number> — integer within bounds
141
- oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
149
+ const program = new Command("myapp")
150
+ colorizeHelp(program) // applies recursively to all subcommands
142
151
  ```
143
152
 
144
- ## Custom parser type inference
153
+ ## Import paths
145
154
 
146
- When `.option()` is called with a parser function as the third argument, Commander infers the return type:
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
- ```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
- ```
160
+ ## Beyond extra-typings
154
161
 
155
- Default values can be passed as the fourth argument:
162
+ Built on the shoulders of [@commander-js/extra-typings](https://github.com/commander-js/extra-typings). We add:
156
163
 
157
- ```typescript
158
- .option("-p, --port <n>", "Port", parseInt, 8080) // port: number (defaults to 8080)
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
- - [Standard Schema](https://github.com/standard-schema/standard-schema) -- universal schema interop protocol for type-safe validation
165
- - [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional ANSI color detection for respecting NO_COLOR/FORCE_COLOR/terminal capabilities
166
- - Uses Commander's built-in `configureHelp()` style hooks (added in Commander 12) for colorization
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silvery/commander",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Colorized Commander.js help output using ANSI escape codes",
5
5
  "keywords": [
6
6
  "ansi",
package/src/command.ts CHANGED
@@ -1,26 +1,25 @@
1
1
  /**
2
- * Enhanced Commander Command with auto-colorized help and Standard Schema support.
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 and automatic Standard Schema /
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
- * const program = new Command("myapp")
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 automatic Standard Schema / legacy Zod detection.
90
+ * Add an option with smart third-argument detection.
92
91
  *
93
- * When the third argument is a Standard Schema v1 object (Zod >=3.24,
94
- * Valibot >=1.0, ArkType >=2.0, or @silvery/commander presets), it's
95
- * automatically wrapped as a Commander parser function.
96
- *
97
- * When the third argument is a legacy Zod schema (pre-3.24, has `_def`
98
- * and `parse` but no `~standard`), it's also wrapped automatically.
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 and Standard Schema support
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
- // Presets and Standard Schema type
10
- export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange, oneOf } from "./presets.ts"
11
- export type { Preset, StandardSchemaV1 } from "./presets.ts"
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
- * Pre-built Standard Schema v1 presets for common CLI argument patterns.
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 preset implements Standard Schema v1 for interop with any schema library,
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 { createCLI, port, csv, int, url, oneOf } from "@silvery/commander"
10
+ * import { Command, port, csv } from "@silvery/commander"
11
11
  *
12
- * const cli = createCLI("deploy")
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("--callback <url>", "Callback", url) // string (validated URL)
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 preset with standalone parse/safeParse methods. */
44
- export interface Preset<T> extends StandardSchemaV1<T> {
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 createPreset<T>(vendor: string, validate: (value: unknown) => T): Preset<T> {
52
- const schema: Preset<T> = {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<number>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<string[]>(VENDOR, (v) => {
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 = createPreset<unknown>(VENDOR, (v) => {
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 = createPreset<boolean>(VENDOR, (v) => {
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 = createPreset<Date>(VENDOR, (v) => {
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 = createPreset<string>(VENDOR, (v) => {
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 = createPreset<RegExp>(VENDOR, (v) => {
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): Preset<number> {
182
- return createPreset<number>(VENDOR, (v) => {
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 presets.
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 only evaluated if user imports `z`.
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
- * const program = new Command("deploy")
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 presets (built on Zod schemas)
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
  }