@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 CHANGED
@@ -1,169 +1,76 @@
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
+ 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
- ## Three layers
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
- ## Usage
9
+ ## Example
19
10
 
20
11
  ```typescript
21
- import { Command, port, csv, oneOf } from "@silvery/commander"
12
+ import { Command, z } from "@silvery/commander"
22
13
 
23
14
  const program = new Command("deploy")
24
- .description("Deploy the application")
15
+ .description("Deploy to an environment")
25
16
  .version("1.0.0")
26
- .option("-p, --port <n>", "Port", port) // number (1-65535)
27
- .option("--tags <t>", "Tags", csv) // string[]
28
- .option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
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 opts = program.opts()
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
- Help output is automatically colorized using Commander's built-in `configureHelp()` style hooks (headings bold, flags green, commands cyan, descriptions dim, arguments yellow).
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
- You can also use `colorizeHelp()` standalone with a plain Commander `Command`:
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
- ```typescript
39
- import { Command } from "commander"
40
- import { colorizeHelp } from "@silvery/commander"
36
+ <pre><code>$ deploy --help
41
37
 
42
- const program = new Command("myapp").description("My CLI tool")
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
- 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:
40
+ Deploy to an environment
49
41
 
50
- ```typescript
51
- import { Command } from "@silvery/commander"
52
- import { z } from "zod"
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 &lt;env&gt;</span> <span style="color:#888">Target environment</span>
45
+ <span style="color:#98c379">-p, --port &lt;n&gt;</span> <span style="color:#888">Port number</span>
46
+ <span style="color:#98c379">-r, --retries &lt;n&gt;</span> <span style="color:#888">Retry count</span>
47
+ <span style="color:#98c379">--tags &lt;t&gt;</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
- const program = new Command("deploy")
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
- 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.
54
+ ## What's included
65
55
 
66
- ## Zod CLI presets
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
- Import `z` from `@silvery/commander` for an extended Zod object with CLI-specific schemas:
64
+ ## Docs
69
65
 
70
- ```typescript
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 -- 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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silvery/commander",
3
- "version": "0.4.0",
3
+ "version": "0.6.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,21 +87,26 @@ 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
- 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 and Standard Schema support
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
- // 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
  }