@silvery/commander 0.3.0 → 0.4.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 +91 -120
- package/package.json +6 -18
- package/src/colorize.ts +164 -0
- package/src/command.ts +117 -0
- package/src/index.ts +10 -172
- package/src/presets.ts +18 -3
- package/src/z.ts +45 -0
- package/src/typed.ts +0 -465
package/README.md
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
|
|
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`.
|
|
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"
|
|
16
|
+
```
|
|
4
17
|
|
|
5
18
|
## Usage
|
|
6
19
|
|
|
7
20
|
```typescript
|
|
8
|
-
import {
|
|
21
|
+
import { Command, port, csv, oneOf } from "@silvery/commander"
|
|
9
22
|
|
|
10
|
-
const
|
|
11
|
-
.description("
|
|
23
|
+
const program = new Command("deploy")
|
|
24
|
+
.description("Deploy the application")
|
|
12
25
|
.version("1.0.0")
|
|
13
|
-
.option("-
|
|
14
|
-
.option("
|
|
15
|
-
.option("-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { verbose, port, output, color } = cli.opts()
|
|
20
|
-
// ^boolean ^number ^string|true ^boolean
|
|
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"]))
|
|
29
|
+
|
|
30
|
+
program.parse()
|
|
31
|
+
const opts = program.opts()
|
|
21
32
|
```
|
|
22
33
|
|
|
23
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,19 +43,57 @@ const program = new Command("myapp").description("My CLI tool")
|
|
|
32
43
|
colorizeHelp(program) // applies recursively to all subcommands
|
|
33
44
|
```
|
|
34
45
|
|
|
46
|
+
## Standard Schema validation
|
|
47
|
+
|
|
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:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Command } from "@silvery/commander"
|
|
52
|
+
import { z } from "zod"
|
|
53
|
+
|
|
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
|
+
```
|
|
63
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
## Zod CLI presets
|
|
67
|
+
|
|
68
|
+
Import `z` from `@silvery/commander` for an extended Zod object with CLI-specific schemas:
|
|
69
|
+
|
|
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
|
+
|
|
35
84
|
## Presets
|
|
36
85
|
|
|
37
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.
|
|
38
87
|
|
|
39
88
|
```typescript
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
.option("-p, --port <n>", "Port", port)
|
|
44
|
-
.option("-r, --retries <n>", "Retries", int)
|
|
45
|
-
.option("--tags <t>", "Tags", csv)
|
|
46
|
-
.option("--callback <url>", "Callback", url)
|
|
47
|
-
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"]))
|
|
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"]))
|
|
48
97
|
```
|
|
49
98
|
|
|
50
99
|
### Standalone usage
|
|
@@ -55,7 +104,7 @@ Presets also work outside Commander for validating env vars, config files, etc.
|
|
|
55
104
|
import { port, csv, oneOf } from "@silvery/commander/parse"
|
|
56
105
|
|
|
57
106
|
// .parse() — returns value or throws
|
|
58
|
-
const dbPort = port.parse(process.env.DB_PORT ?? "5432")
|
|
107
|
+
const dbPort = port.parse(process.env.DB_PORT ?? "5432") // 3000
|
|
59
108
|
|
|
60
109
|
// .safeParse() — returns result object, never throws
|
|
61
110
|
const result = port.safeParse("abc")
|
|
@@ -68,128 +117,50 @@ const validated = port["~standard"].validate("8080")
|
|
|
68
117
|
|
|
69
118
|
### Available presets
|
|
70
119
|
|
|
71
|
-
| Preset
|
|
72
|
-
|
|
|
73
|
-
| `int`
|
|
74
|
-
| `uint`
|
|
75
|
-
| `float` | `number`
|
|
76
|
-
| `port`
|
|
77
|
-
| `url`
|
|
78
|
-
| `path`
|
|
79
|
-
| `csv`
|
|
80
|
-
| `json`
|
|
81
|
-
| `bool`
|
|
82
|
-
| `date`
|
|
83
|
-
| `email` | `string`
|
|
84
|
-
| `regex` | `RegExp`
|
|
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 |
|
|
85
134
|
|
|
86
135
|
### Factory presets
|
|
87
136
|
|
|
88
137
|
```typescript
|
|
89
138
|
import { intRange, oneOf } from "@silvery/commander"
|
|
90
139
|
|
|
91
|
-
intRange(1, 100)
|
|
140
|
+
intRange(1, 100) // Preset<number> — integer within bounds
|
|
92
141
|
oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
|
|
93
142
|
```
|
|
94
143
|
|
|
95
144
|
## Custom parser type inference
|
|
96
145
|
|
|
97
|
-
When `.option()` is called with a parser function as the third argument, the return type
|
|
146
|
+
When `.option()` is called with a parser function as the third argument, Commander infers the return type:
|
|
98
147
|
|
|
99
148
|
```typescript
|
|
100
|
-
const
|
|
101
|
-
.option("-p, --port <n>", "Port", parseInt) //
|
|
102
|
-
.option("-t, --timeout <ms>", "Timeout", Number) //
|
|
103
|
-
.option("--tags <items>", "Tags", (v) => v.split(",")) //
|
|
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[]
|
|
104
153
|
```
|
|
105
154
|
|
|
106
155
|
Default values can be passed as the fourth argument:
|
|
107
156
|
|
|
108
157
|
```typescript
|
|
109
|
-
.option("-p, --port <n>", "Port", parseInt, 8080) //
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Standard Schema validation
|
|
113
|
-
|
|
114
|
-
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) compatible schema as the third argument for combined parsing, validation, and type inference. This works with Zod (>=3.24), Valibot (>=1.0), ArkType (>=2.0), and any other library implementing the standard:
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
import { z } from "zod"
|
|
118
|
-
|
|
119
|
-
const cli = createCLI("deploy")
|
|
120
|
-
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
121
|
-
// → port: number (validated at parse time)
|
|
122
|
-
|
|
123
|
-
.option("-e, --env <env>", "Environment", z.enum(["dev", "staging", "prod"]))
|
|
124
|
-
// → env: "dev" | "staging" | "prod" (union type)
|
|
125
|
-
|
|
126
|
-
.option(
|
|
127
|
-
"--tags <t>",
|
|
128
|
-
"Tags",
|
|
129
|
-
z.string().transform((v) => v.split(",")),
|
|
130
|
-
)
|
|
131
|
-
// → tags: string[] (transformed)
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Schema libraries are optional peer dependencies -- detected at runtime via the Standard Schema `~standard` interface, never imported at the top level. A legacy fallback supports older Zod versions (pre-3.24) that don't implement Standard Schema yet.
|
|
135
|
-
|
|
136
|
-
## Typed action handlers
|
|
137
|
-
|
|
138
|
-
Action callbacks receive typed arguments and options:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
const cli = createCLI("deploy")
|
|
142
|
-
.argument("<env>", "Target environment")
|
|
143
|
-
.argument("[tag]", "Optional deploy tag")
|
|
144
|
-
.option("-f, --force", "Force deploy")
|
|
145
|
-
.action((env, tag, opts) => {
|
|
146
|
-
// env: string, tag: string | undefined, opts: { force: boolean | undefined }
|
|
147
|
-
})
|
|
158
|
+
.option("-p, --port <n>", "Port", parseInt, 8080) // port: number (defaults to 8080)
|
|
148
159
|
```
|
|
149
160
|
|
|
150
|
-
Required arguments (`<name>`) are `string`, optional arguments (`[name]`) are `string | undefined`.
|
|
151
|
-
|
|
152
|
-
## Choices narrowing
|
|
153
|
-
|
|
154
|
-
Use `.optionWithChoices()` to restrict an option to a fixed set of values with union type inference:
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
const cli = createCLI("deploy").optionWithChoices("-e, --env <env>", "Environment", ["dev", "staging", "prod"] as const)
|
|
158
|
-
// → env: "dev" | "staging" | "prod" | undefined
|
|
159
|
-
|
|
160
|
-
cli.parse()
|
|
161
|
-
const { env } = cli.opts() // env: "dev" | "staging" | "prod" | undefined
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Commander validates the choice at parse time and rejects invalid values.
|
|
165
|
-
|
|
166
|
-
## Environment variable support
|
|
167
|
-
|
|
168
|
-
Chain `.env()` to set an environment variable fallback for the last-added option:
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
.option("-p, --port <n>", "Port").env("PORT")
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Improvements over @commander-js/extra-typings
|
|
175
|
-
|
|
176
|
-
| Feature | extra-typings | @silvery/commander |
|
|
177
|
-
| ---------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
178
|
-
| Type inference | 1536-line .d.ts with recursive generic accumulation | ~120 lines using TS 5.4+ const type params + template literals |
|
|
179
|
-
| Custom parser types | Yes (.option with parseFloat -> number) | Yes (parser return type inference) |
|
|
180
|
-
| Standard Schema | No | Yes (Zod, Valibot, ArkType, or any Standard Schema v1 library) |
|
|
181
|
-
| Built-in presets | No | Yes (port, int, csv, url, oneOf, etc.) |
|
|
182
|
-
| Typed action handlers | Yes (full signature inference) | Yes (arguments + options) |
|
|
183
|
-
| Choices narrowing | Via .addOption() | Via .optionWithChoices() |
|
|
184
|
-
| Colorized help | Not included | Built-in via Commander's native style hooks |
|
|
185
|
-
| Package size | Types only (25 lines runtime) | Types + colorizer + schemas (~500 lines, zero required deps) |
|
|
186
|
-
| Installation | Separate package alongside commander | Single package, re-exports Commander |
|
|
187
|
-
| Negated flags (--no-X) | Partial | Key extraction + boolean type inference |
|
|
188
|
-
|
|
189
161
|
## Credits
|
|
190
162
|
|
|
191
163
|
- [Commander.js](https://github.com/tj/commander.js) by TJ Holowaychuk and contributors -- the underlying CLI framework
|
|
192
|
-
- [@commander-js/extra-typings](https://github.com/commander-js/extra-typings) -- inspired the type inference approach; our implementation uses modern TypeScript features (const type parameters, template literal types) to achieve similar results in fewer lines
|
|
193
164
|
- [Standard Schema](https://github.com/standard-schema/standard-schema) -- universal schema interop protocol for type-safe validation
|
|
194
165
|
- [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional ANSI color detection for respecting NO_COLOR/FORCE_COLOR/terminal capabilities
|
|
195
166
|
- Uses Commander's built-in `configureHelp()` style hooks (added in Commander 12) for colorization
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Colorized Commander.js help output using ANSI escape codes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
23
24
|
"exports": {
|
|
24
25
|
".": "./src/index.ts",
|
|
25
26
|
"./parse": "./src/presets.ts"
|
|
@@ -27,32 +28,19 @@
|
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
|
29
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": ">=12.0.0"
|
|
33
|
+
},
|
|
30
34
|
"peerDependencies": {
|
|
31
|
-
"@commander-js/extra-typings": ">=12.0.0",
|
|
32
35
|
"@silvery/ansi": ">=0.1.0",
|
|
33
|
-
"
|
|
34
|
-
"zod": ">=3.0.0",
|
|
35
|
-
"valibot": ">=1.0.0",
|
|
36
|
-
"arktype": ">=2.0.0"
|
|
36
|
+
"zod": ">=3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
|
-
"commander": {
|
|
40
|
-
"optional": true
|
|
41
|
-
},
|
|
42
|
-
"@commander-js/extra-typings": {
|
|
43
|
-
"optional": true
|
|
44
|
-
},
|
|
45
39
|
"@silvery/ansi": {
|
|
46
40
|
"optional": true
|
|
47
41
|
},
|
|
48
42
|
"zod": {
|
|
49
43
|
"optional": true
|
|
50
|
-
},
|
|
51
|
-
"valibot": {
|
|
52
|
-
"optional": true
|
|
53
|
-
},
|
|
54
|
-
"arktype": {
|
|
55
|
-
"optional": true
|
|
56
44
|
}
|
|
57
45
|
},
|
|
58
46
|
"engines": {
|
package/src/colorize.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander.js help colorization using ANSI escape codes.
|
|
3
|
+
*
|
|
4
|
+
* Uses Commander's built-in style hooks (styleTitle, styleOptionText, etc.)
|
|
5
|
+
* rather than regex post-processing. Works with @silvery/commander
|
|
6
|
+
* or plain commander — accepts a minimal CommandLike interface so Commander
|
|
7
|
+
* is a peer dependency, not a hard one.
|
|
8
|
+
*
|
|
9
|
+
* Zero dependencies — only raw ANSI escape codes.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Command } from "@silvery/commander"
|
|
14
|
+
* import { colorizeHelp } from "@silvery/commander"
|
|
15
|
+
*
|
|
16
|
+
* const program = new Command("myapp").description("My CLI tool")
|
|
17
|
+
* colorizeHelp(program)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Raw ANSI escape codes — no framework dependencies.
|
|
22
|
+
const RESET = "\x1b[0m"
|
|
23
|
+
const BOLD = "\x1b[1m"
|
|
24
|
+
const DIM = "\x1b[2m"
|
|
25
|
+
const CYAN = "\x1b[36m"
|
|
26
|
+
const GREEN = "\x1b[32m"
|
|
27
|
+
const YELLOW = "\x1b[33m"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if color output should be enabled.
|
|
31
|
+
* Uses @silvery/ansi detectColor() if available, falls back to basic
|
|
32
|
+
* NO_COLOR/FORCE_COLOR/isTTY checks.
|
|
33
|
+
*/
|
|
34
|
+
let _shouldColorize: boolean | undefined
|
|
35
|
+
|
|
36
|
+
export function shouldColorize(): boolean {
|
|
37
|
+
if (_shouldColorize !== undefined) return _shouldColorize
|
|
38
|
+
|
|
39
|
+
// Try @silvery/ansi for full detection (respects NO_COLOR, FORCE_COLOR, TERM, etc.)
|
|
40
|
+
try {
|
|
41
|
+
const { detectColor } = require("@silvery/ansi") as { detectColor: (stdout: NodeJS.WriteStream) => string | null }
|
|
42
|
+
_shouldColorize = detectColor(process.stdout) !== null
|
|
43
|
+
} catch {
|
|
44
|
+
// Fallback: basic NO_COLOR / FORCE_COLOR / isTTY checks
|
|
45
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
46
|
+
_shouldColorize = false
|
|
47
|
+
} else if (process.env.FORCE_COLOR !== undefined) {
|
|
48
|
+
_shouldColorize = true
|
|
49
|
+
} else {
|
|
50
|
+
_shouldColorize = process.stdout?.isTTY ?? true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return _shouldColorize
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Wrap a string with ANSI codes, handling nested resets. */
|
|
58
|
+
function ansi(text: string, code: string): string {
|
|
59
|
+
return `${code}${text}${RESET}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Minimal interface for Commander's Command — avoids requiring Commander
|
|
64
|
+
* as a direct dependency. Works with both `commander` and
|
|
65
|
+
* `@silvery/commander`.
|
|
66
|
+
*
|
|
67
|
+
* Uses permissive types to ensure structural compatibility with all
|
|
68
|
+
* Commander versions, overloads, and generic instantiations.
|
|
69
|
+
*/
|
|
70
|
+
export interface CommandLike {
|
|
71
|
+
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
72
|
+
configureHelp(...args: any[]): any
|
|
73
|
+
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
74
|
+
configureOutput(...args: any[]): any
|
|
75
|
+
// biome-ignore lint: permissive to match Commander's Command[] structurally
|
|
76
|
+
readonly commands: readonly any[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Color scheme for help output. Values are raw ANSI escape sequences. */
|
|
80
|
+
export interface ColorizeHelpOptions {
|
|
81
|
+
/** ANSI code for command/subcommand names. Default: cyan */
|
|
82
|
+
commands?: string
|
|
83
|
+
/** ANSI code for --flags and -short options. Default: green */
|
|
84
|
+
flags?: string
|
|
85
|
+
/** ANSI code for description text. Default: dim */
|
|
86
|
+
description?: string
|
|
87
|
+
/** ANSI code for section headings (Usage:, Options:, etc.). Default: bold */
|
|
88
|
+
heading?: string
|
|
89
|
+
/** ANSI code for <required> and [optional] argument brackets. Default: yellow */
|
|
90
|
+
brackets?: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply colorized help output to a Commander.js program and all its subcommands.
|
|
95
|
+
*
|
|
96
|
+
* Uses Commander's built-in `configureHelp()` style hooks rather than
|
|
97
|
+
* post-processing the formatted string. This approach is robust against
|
|
98
|
+
* formatting changes in Commander and handles wrapping correctly.
|
|
99
|
+
*
|
|
100
|
+
* @param program - A Commander Command instance (or compatible object)
|
|
101
|
+
* @param options - Override default ANSI color codes for each element
|
|
102
|
+
*/
|
|
103
|
+
export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions): void {
|
|
104
|
+
const cmds = options?.commands ?? CYAN
|
|
105
|
+
const flags = options?.flags ?? GREEN
|
|
106
|
+
const desc = options?.description ?? DIM
|
|
107
|
+
const heading = options?.heading ?? BOLD
|
|
108
|
+
const brackets = options?.brackets ?? YELLOW
|
|
109
|
+
|
|
110
|
+
const helpConfig: Record<string, unknown> = {
|
|
111
|
+
// Section headings: "Usage:", "Options:", "Commands:", "Arguments:"
|
|
112
|
+
styleTitle(str: string): string {
|
|
113
|
+
return ansi(str, heading)
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Command name in usage line and subcommand terms
|
|
117
|
+
styleCommandText(str: string): string {
|
|
118
|
+
return ansi(str, cmds)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Option terms: "-v, --verbose", "--repo <path>", "[options]"
|
|
122
|
+
styleOptionText(str: string): string {
|
|
123
|
+
return ansi(str, flags)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Subcommand names in the commands list
|
|
127
|
+
styleSubcommandText(str: string): string {
|
|
128
|
+
return ansi(str, cmds)
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Argument terms: "<file>", "[dir]"
|
|
132
|
+
styleArgumentText(str: string): string {
|
|
133
|
+
return ansi(str, brackets)
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Description text for options, subcommands, arguments
|
|
137
|
+
styleDescriptionText(str: string): string {
|
|
138
|
+
return ansi(str, desc)
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Command description (the main program description line) — keep normal
|
|
142
|
+
styleCommandDescription(str: string): string {
|
|
143
|
+
return str
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
program.configureHelp(helpConfig)
|
|
148
|
+
|
|
149
|
+
// Tell Commander that color output is supported, even when stdout is not
|
|
150
|
+
// a TTY (e.g., piped output, CI, tests). Without this, Commander strips
|
|
151
|
+
// all ANSI codes from helpInformation() output.
|
|
152
|
+
//
|
|
153
|
+
// Callers who want to respect NO_COLOR/FORCE_COLOR should check
|
|
154
|
+
// shouldColorize() before calling colorizeHelp().
|
|
155
|
+
program.configureOutput({
|
|
156
|
+
getOutHasColors: () => true,
|
|
157
|
+
getErrHasColors: () => true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Apply recursively to all existing subcommands
|
|
161
|
+
for (const sub of program.commands) {
|
|
162
|
+
colorizeHelp(sub, options)
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Commander Command with auto-colorized help and Standard Schema support.
|
|
3
|
+
*
|
|
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()`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Command } from "@silvery/commander"
|
|
11
|
+
* import { port, csv } from "@silvery/commander/parse"
|
|
12
|
+
*
|
|
13
|
+
* const program = new Command("myapp")
|
|
14
|
+
* .description("My CLI tool")
|
|
15
|
+
* .version("1.0.0")
|
|
16
|
+
* .option("-p, --port <n>", "Port", port)
|
|
17
|
+
* .option("--tags <t>", "Tags", csv)
|
|
18
|
+
*
|
|
19
|
+
* program.parse()
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Command as BaseCommand } from "commander"
|
|
24
|
+
import { colorizeHelp } from "./colorize.ts"
|
|
25
|
+
import type { StandardSchemaV1 } from "./presets.ts"
|
|
26
|
+
|
|
27
|
+
// --- Standard Schema support ---
|
|
28
|
+
|
|
29
|
+
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
30
|
+
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
31
|
+
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Wrap a Standard Schema as a Commander parser function */
|
|
35
|
+
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
36
|
+
return (value: string) => {
|
|
37
|
+
const result = schema["~standard"].validate(value)
|
|
38
|
+
if ("issues" in result) {
|
|
39
|
+
const msg = result.issues.map((i) => i.message).join(", ")
|
|
40
|
+
throw new Error(msg)
|
|
41
|
+
}
|
|
42
|
+
return result.value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
50
|
+
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
51
|
+
*/
|
|
52
|
+
interface ZodLike<T = any> {
|
|
53
|
+
parse(value: unknown): T
|
|
54
|
+
_def: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
58
|
+
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
59
|
+
return (
|
|
60
|
+
typeof value === "object" &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
typeof (value as any).parse === "function" &&
|
|
63
|
+
"_def" in (value as any) &&
|
|
64
|
+
!("~standard" in (value as any))
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
69
|
+
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
70
|
+
return (value: string) => {
|
|
71
|
+
try {
|
|
72
|
+
return schema.parse(value)
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
// Format Zod errors as Commander-style messages
|
|
75
|
+
if (err?.issues) {
|
|
76
|
+
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
77
|
+
throw new Error(messages)
|
|
78
|
+
}
|
|
79
|
+
throw err
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class Command extends BaseCommand {
|
|
85
|
+
constructor(name?: string) {
|
|
86
|
+
super(name)
|
|
87
|
+
colorizeHelp(this as any)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add an option with automatic Standard Schema / legacy Zod detection.
|
|
92
|
+
*
|
|
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.
|
|
99
|
+
*/
|
|
100
|
+
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): this {
|
|
101
|
+
if (isStandardSchema(parseArgOrDefault)) {
|
|
102
|
+
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
103
|
+
}
|
|
104
|
+
if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
105
|
+
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
106
|
+
}
|
|
107
|
+
if (typeof parseArgOrDefault === "function") {
|
|
108
|
+
return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
109
|
+
}
|
|
110
|
+
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Subcommands also get colorized help and Standard Schema support
|
|
114
|
+
createCommand(name?: string): Command {
|
|
115
|
+
return new Command(name)
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,176 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
export { Command
|
|
3
|
-
export type
|
|
1
|
+
// Enhanced Commander
|
|
2
|
+
export { Command } from "./command.ts"
|
|
3
|
+
export { colorizeHelp, shouldColorize, type ColorizeHelpOptions, type CommandLike } from "./colorize.ts"
|
|
4
4
|
|
|
5
|
-
// Re-export
|
|
6
|
-
export {
|
|
7
|
-
export type {
|
|
5
|
+
// Re-export Commander's other classes
|
|
6
|
+
export { Option, Argument, CommanderError, InvalidArgumentError, Help } from "commander"
|
|
7
|
+
export type { OptionValues } from "commander"
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Presets and Standard Schema type
|
|
10
10
|
export { int, uint, float, port, url, path, csv, json, bool, date, email, regex, intRange, oneOf } from "./presets.ts"
|
|
11
|
-
export type { Preset } from "./presets.ts"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Commander.js help colorization using ANSI escape codes.
|
|
15
|
-
*
|
|
16
|
-
* Uses Commander's built-in style hooks (styleTitle, styleOptionText, etc.)
|
|
17
|
-
* rather than regex post-processing. Works with @silvery/commander
|
|
18
|
-
* or plain commander — accepts a minimal CommandLike interface so Commander
|
|
19
|
-
* is a peer dependency, not a hard one.
|
|
20
|
-
*
|
|
21
|
-
* Zero dependencies — only raw ANSI escape codes.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* import { Command } from "@silvery/commander"
|
|
26
|
-
* import { colorizeHelp } from "@silvery/commander"
|
|
27
|
-
*
|
|
28
|
-
* const program = new Command("myapp").description("My CLI tool")
|
|
29
|
-
* colorizeHelp(program)
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
// Raw ANSI escape codes — no framework dependencies.
|
|
34
|
-
const RESET = "\x1b[0m"
|
|
35
|
-
const BOLD = "\x1b[1m"
|
|
36
|
-
const DIM = "\x1b[2m"
|
|
37
|
-
const CYAN = "\x1b[36m"
|
|
38
|
-
const GREEN = "\x1b[32m"
|
|
39
|
-
const YELLOW = "\x1b[33m"
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if color output should be enabled.
|
|
43
|
-
* Uses @silvery/ansi detectColor() if available, falls back to basic
|
|
44
|
-
* NO_COLOR/FORCE_COLOR/isTTY checks.
|
|
45
|
-
*/
|
|
46
|
-
let _shouldColorize: boolean | undefined
|
|
47
|
-
|
|
48
|
-
export function shouldColorize(): boolean {
|
|
49
|
-
if (_shouldColorize !== undefined) return _shouldColorize
|
|
50
|
-
|
|
51
|
-
// Try @silvery/ansi for full detection (respects NO_COLOR, FORCE_COLOR, TERM, etc.)
|
|
52
|
-
try {
|
|
53
|
-
const { detectColor } = require("@silvery/ansi") as { detectColor: (stdout: NodeJS.WriteStream) => string | null }
|
|
54
|
-
_shouldColorize = detectColor(process.stdout) !== null
|
|
55
|
-
} catch {
|
|
56
|
-
// Fallback: basic NO_COLOR / FORCE_COLOR / isTTY checks
|
|
57
|
-
if (process.env.NO_COLOR !== undefined) {
|
|
58
|
-
_shouldColorize = false
|
|
59
|
-
} else if (process.env.FORCE_COLOR !== undefined) {
|
|
60
|
-
_shouldColorize = true
|
|
61
|
-
} else {
|
|
62
|
-
_shouldColorize = process.stdout?.isTTY ?? true
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return _shouldColorize
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Wrap a string with ANSI codes, handling nested resets. */
|
|
70
|
-
function ansi(text: string, code: string): string {
|
|
71
|
-
return `${code}${text}${RESET}`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Minimal interface for Commander's Command — avoids requiring Commander
|
|
76
|
-
* as a direct dependency. Works with both `commander` and
|
|
77
|
-
* `@silvery/commander`.
|
|
78
|
-
*
|
|
79
|
-
* Uses permissive types to ensure structural compatibility with all
|
|
80
|
-
* Commander versions, overloads, and generic instantiations.
|
|
81
|
-
*/
|
|
82
|
-
export interface CommandLike {
|
|
83
|
-
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
84
|
-
configureHelp(...args: any[]): any
|
|
85
|
-
// biome-ignore lint: permissive to match Commander's overloaded signatures
|
|
86
|
-
configureOutput(...args: any[]): any
|
|
87
|
-
// biome-ignore lint: permissive to match Commander's Command[] structurally
|
|
88
|
-
readonly commands: readonly any[]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Color scheme for help output. Values are raw ANSI escape sequences. */
|
|
92
|
-
export interface ColorizeHelpOptions {
|
|
93
|
-
/** ANSI code for command/subcommand names. Default: cyan */
|
|
94
|
-
commands?: string
|
|
95
|
-
/** ANSI code for --flags and -short options. Default: green */
|
|
96
|
-
flags?: string
|
|
97
|
-
/** ANSI code for description text. Default: dim */
|
|
98
|
-
description?: string
|
|
99
|
-
/** ANSI code for section headings (Usage:, Options:, etc.). Default: bold */
|
|
100
|
-
heading?: string
|
|
101
|
-
/** ANSI code for <required> and [optional] argument brackets. Default: yellow */
|
|
102
|
-
brackets?: string
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Apply colorized help output to a Commander.js program and all its subcommands.
|
|
107
|
-
*
|
|
108
|
-
* Uses Commander's built-in `configureHelp()` style hooks rather than
|
|
109
|
-
* post-processing the formatted string. This approach is robust against
|
|
110
|
-
* formatting changes in Commander and handles wrapping correctly.
|
|
111
|
-
*
|
|
112
|
-
* @param program - A Commander Command instance (or compatible object)
|
|
113
|
-
* @param options - Override default ANSI color codes for each element
|
|
114
|
-
*/
|
|
115
|
-
export function colorizeHelp(program: CommandLike, options?: ColorizeHelpOptions): void {
|
|
116
|
-
const cmds = options?.commands ?? CYAN
|
|
117
|
-
const flags = options?.flags ?? GREEN
|
|
118
|
-
const desc = options?.description ?? DIM
|
|
119
|
-
const heading = options?.heading ?? BOLD
|
|
120
|
-
const brackets = options?.brackets ?? YELLOW
|
|
121
|
-
|
|
122
|
-
const helpConfig: Record<string, unknown> = {
|
|
123
|
-
// Section headings: "Usage:", "Options:", "Commands:", "Arguments:"
|
|
124
|
-
styleTitle(str: string): string {
|
|
125
|
-
return ansi(str, heading)
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// Command name in usage line and subcommand terms
|
|
129
|
-
styleCommandText(str: string): string {
|
|
130
|
-
return ansi(str, cmds)
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
// Option terms: "-v, --verbose", "--repo <path>", "[options]"
|
|
134
|
-
styleOptionText(str: string): string {
|
|
135
|
-
return ansi(str, flags)
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
// Subcommand names in the commands list
|
|
139
|
-
styleSubcommandText(str: string): string {
|
|
140
|
-
return ansi(str, cmds)
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
// Argument terms: "<file>", "[dir]"
|
|
144
|
-
styleArgumentText(str: string): string {
|
|
145
|
-
return ansi(str, brackets)
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
// Description text for options, subcommands, arguments
|
|
149
|
-
styleDescriptionText(str: string): string {
|
|
150
|
-
return ansi(str, desc)
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
// Command description (the main program description line) — keep normal
|
|
154
|
-
styleCommandDescription(str: string): string {
|
|
155
|
-
return str
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
program.configureHelp(helpConfig)
|
|
160
|
-
|
|
161
|
-
// Tell Commander that color output is supported, even when stdout is not
|
|
162
|
-
// a TTY (e.g., piped output, CI, tests). Without this, Commander strips
|
|
163
|
-
// all ANSI codes from helpInformation() output.
|
|
164
|
-
//
|
|
165
|
-
// Callers who want to respect NO_COLOR/FORCE_COLOR should check
|
|
166
|
-
// shouldColorize() before calling colorizeHelp().
|
|
167
|
-
program.configureOutput({
|
|
168
|
-
getOutHasColors: () => true,
|
|
169
|
-
getErrHasColors: () => true,
|
|
170
|
-
})
|
|
11
|
+
export type { Preset, StandardSchemaV1 } from "./presets.ts"
|
|
171
12
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
colorizeHelp(sub, options)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
13
|
+
// Tree-shakeable: only evaluated if user imports z
|
|
14
|
+
export { z } from "./z.ts"
|
package/src/presets.ts
CHANGED
|
@@ -22,7 +22,23 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Standard Schema v1 interface — the universal schema interop protocol.
|
|
27
|
+
* Supports any schema library that implements Standard Schema (Zod >=3.24,
|
|
28
|
+
* Valibot >=1.0, ArkType >=2.0, etc.).
|
|
29
|
+
*
|
|
30
|
+
* Inlined to avoid any dependency on @standard-schema/spec.
|
|
31
|
+
* See: https://github.com/standard-schema/standard-schema
|
|
32
|
+
*/
|
|
33
|
+
export interface StandardSchemaV1<T = unknown> {
|
|
34
|
+
readonly "~standard": {
|
|
35
|
+
readonly version: 1
|
|
36
|
+
readonly vendor: string
|
|
37
|
+
readonly validate: (
|
|
38
|
+
value: unknown,
|
|
39
|
+
) => { value: T } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }> }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
26
42
|
|
|
27
43
|
/** A Standard Schema v1 preset with standalone parse/safeParse methods. */
|
|
28
44
|
export interface Preset<T> extends StandardSchemaV1<T> {
|
|
@@ -165,8 +181,7 @@ export const regex = createPreset<RegExp>(VENDOR, (v) => {
|
|
|
165
181
|
export function intRange(min: number, max: number): Preset<number> {
|
|
166
182
|
return createPreset<number>(VENDOR, (v) => {
|
|
167
183
|
const n = Number(v)
|
|
168
|
-
if (!Number.isInteger(n) || n < min || n > max)
|
|
169
|
-
throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
184
|
+
if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`)
|
|
170
185
|
return n
|
|
171
186
|
})
|
|
172
187
|
}
|
package/src/z.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended Zod object with CLI presets.
|
|
3
|
+
*
|
|
4
|
+
* Spreads all of Zod's exports and adds CLI-specific schemas.
|
|
5
|
+
* Tree-shakeable — only evaluated if user imports `z`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { z, Command } from "@silvery/commander"
|
|
10
|
+
*
|
|
11
|
+
* const program = new Command("deploy")
|
|
12
|
+
* .option("-p, --port <n>", "Port", z.port)
|
|
13
|
+
* .option("-e, --env <e>", "Environment", z.oneOf(["dev", "staging", "prod"]))
|
|
14
|
+
* .option("--tags <t>", "Tags", z.csv)
|
|
15
|
+
*
|
|
16
|
+
* program.parse()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as zod from "zod"
|
|
21
|
+
|
|
22
|
+
export const z = {
|
|
23
|
+
...zod,
|
|
24
|
+
// CLI presets (built on Zod schemas)
|
|
25
|
+
port: zod.coerce.number().int().min(1).max(65535),
|
|
26
|
+
int: zod.coerce.number().int(),
|
|
27
|
+
uint: zod.coerce.number().int().min(0),
|
|
28
|
+
float: zod.coerce.number(),
|
|
29
|
+
csv: zod.string().transform((v: string) =>
|
|
30
|
+
v
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((s: string) => s.trim())
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
),
|
|
35
|
+
url: zod.string().url(),
|
|
36
|
+
path: zod.string().min(1),
|
|
37
|
+
email: zod.string().email(),
|
|
38
|
+
date: zod.coerce.date(),
|
|
39
|
+
json: zod.string().transform((v: string) => JSON.parse(v)),
|
|
40
|
+
bool: zod
|
|
41
|
+
.enum(["true", "false", "1", "0", "yes", "no", "y", "n"] as const)
|
|
42
|
+
.transform((v: string) => ["true", "1", "yes", "y"].includes(v)),
|
|
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
|
+
}
|
package/src/typed.ts
DELETED
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type-safe Commander.js wrapper — replaces @commander-js/extra-typings.
|
|
3
|
-
*
|
|
4
|
-
* Uses TypeScript 5.4+ const type parameters and template literal types
|
|
5
|
-
* to infer option types from .option() calls. Inspired by
|
|
6
|
-
* @commander-js/extra-typings, which achieves similar results with a
|
|
7
|
-
* 1536-line .d.ts using recursive generic accumulation. This
|
|
8
|
-
* implementation achieves the same inference in ~100 lines of type-level
|
|
9
|
-
* code by leveraging modern TS features (const type params, template
|
|
10
|
-
* literal types, conditional mapped types).
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* import { createCLI } from "@silvery/commander"
|
|
15
|
-
*
|
|
16
|
-
* const cli = createCLI("myapp")
|
|
17
|
-
* .description("My app")
|
|
18
|
-
* .option("-v, --verbose", "Increase verbosity")
|
|
19
|
-
* .option("-p, --port <number>", "Port to listen on", parseInt)
|
|
20
|
-
* .option("-o, --output [path]", "Output path")
|
|
21
|
-
* .option("--no-color", "Disable color output")
|
|
22
|
-
*
|
|
23
|
-
* cli.parse()
|
|
24
|
-
* const opts = cli.opts()
|
|
25
|
-
* // ^? { verbose: boolean, port: number, output: string | true, color: boolean }
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { Command as BaseCommand, Option, Argument } from "commander"
|
|
30
|
-
import { colorizeHelp } from "./index.ts"
|
|
31
|
-
|
|
32
|
-
// --- Type-level option parsing ---
|
|
33
|
-
//
|
|
34
|
-
// Approach: Each .option() call captures the flags string as a const
|
|
35
|
-
// type parameter. Template literal types extract the flag name and
|
|
36
|
-
// determine the value type (boolean for bare flags, string for <value>,
|
|
37
|
-
// string | true for [value]). The result accumulates via intersection
|
|
38
|
-
// types across chained calls. Prettify<T> flattens the intersections
|
|
39
|
-
// for clean hover output.
|
|
40
|
-
//
|
|
41
|
-
// Negated flags (--no-X) are detected and produce a `X: boolean` key.
|
|
42
|
-
|
|
43
|
-
/** Flatten intersection types for clean hover output */
|
|
44
|
-
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
45
|
-
|
|
46
|
-
/** Check if a flags string is a negated flag like "--no-color" */
|
|
47
|
-
type IsNegated<S extends string> = S extends `${string}--no-${string}` ? true : false
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Extract the option key name from a flags string like "-p, --port <value>".
|
|
51
|
-
*
|
|
52
|
-
* Priority: long flag > short flag. Handles negated flags (--no-X → X),
|
|
53
|
-
* kebab-case conversion (--dry-run → dryRun), and short-only flags (-v → v).
|
|
54
|
-
*/
|
|
55
|
-
type ExtractLongName<S extends string> = S extends `${string}--no-${infer Rest}`
|
|
56
|
-
? Rest extends `${infer Name} ${string}`
|
|
57
|
-
? CamelCase<Name>
|
|
58
|
-
: CamelCase<Rest>
|
|
59
|
-
: S extends `${string}--${infer Rest}`
|
|
60
|
-
? Rest extends `${infer Name} ${string}`
|
|
61
|
-
? CamelCase<Name>
|
|
62
|
-
: CamelCase<Rest>
|
|
63
|
-
: S extends `-${infer Short}`
|
|
64
|
-
? Short extends `${infer C} ${string}`
|
|
65
|
-
? C
|
|
66
|
-
: Short
|
|
67
|
-
: never
|
|
68
|
-
|
|
69
|
-
/** Convert kebab-case to camelCase: "dry-run" → "dryRun" */
|
|
70
|
-
type CamelCase<S extends string> = S extends `${infer A}-${infer B}${infer Rest}`
|
|
71
|
-
? `${A}${Uppercase<B>}${CamelCase<Rest>}`
|
|
72
|
-
: S
|
|
73
|
-
|
|
74
|
-
/** Determine the value type from a flags string */
|
|
75
|
-
type FlagValueType<S extends string> =
|
|
76
|
-
IsNegated<S> extends true
|
|
77
|
-
? boolean // negated flags are always boolean
|
|
78
|
-
: S extends `${string}<${string}>`
|
|
79
|
-
? string // required arg → string
|
|
80
|
-
: S extends `${string}[${string}]`
|
|
81
|
-
? string | true // optional arg → string | true
|
|
82
|
-
: boolean // no arg → boolean
|
|
83
|
-
|
|
84
|
-
/** Add a flag to an options record */
|
|
85
|
-
type AddOption<Opts, Flags extends string, Default = undefined> = Opts & {
|
|
86
|
-
[K in ExtractLongName<Flags>]: Default extends undefined ? FlagValueType<Flags> | undefined : FlagValueType<Flags>
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// --- Type-level argument parsing ---
|
|
90
|
-
|
|
91
|
-
/** Extract whether an argument is required (<name>) or optional ([name]) */
|
|
92
|
-
type ArgType<S extends string> = S extends `<${string}>`
|
|
93
|
-
? string
|
|
94
|
-
: S extends `[${string}]`
|
|
95
|
-
? string | undefined
|
|
96
|
-
: string
|
|
97
|
-
|
|
98
|
-
// --- Typed opts helper (resolves accumulated Opts for action handlers) ---
|
|
99
|
-
/** Resolve accumulated option types for use in action handler signatures */
|
|
100
|
-
export type TypedOpts<Opts> = Prettify<Opts>
|
|
101
|
-
|
|
102
|
-
// --- Standard Schema support (v1) ---
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Standard Schema v1 interface — the universal schema interop protocol.
|
|
106
|
-
* Supports any schema library that implements Standard Schema (Zod >=3.24,
|
|
107
|
-
* Valibot >=1.0, ArkType >=2.0, etc.).
|
|
108
|
-
*
|
|
109
|
-
* Inlined to avoid any dependency on @standard-schema/spec.
|
|
110
|
-
* See: https://github.com/standard-schema/standard-schema
|
|
111
|
-
*/
|
|
112
|
-
export interface StandardSchemaV1<T = unknown> {
|
|
113
|
-
readonly "~standard": {
|
|
114
|
-
readonly version: 1
|
|
115
|
-
readonly vendor: string
|
|
116
|
-
readonly validate: (
|
|
117
|
-
value: unknown,
|
|
118
|
-
) => { value: T } | { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<unknown> }> }
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Type-level extraction: infer the output type from a Standard Schema */
|
|
123
|
-
type InferStandardSchema<S> = S extends StandardSchemaV1<infer T> ? T : never
|
|
124
|
-
|
|
125
|
-
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
126
|
-
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
127
|
-
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Wrap a Standard Schema as a Commander parser function */
|
|
131
|
-
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
132
|
-
return (value: string) => {
|
|
133
|
-
const result = schema["~standard"].validate(value)
|
|
134
|
-
if ("issues" in result) {
|
|
135
|
-
const msg = result.issues.map((i) => i.message).join(", ")
|
|
136
|
-
throw new Error(msg)
|
|
137
|
-
}
|
|
138
|
-
return result.value
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
146
|
-
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
147
|
-
*/
|
|
148
|
-
interface ZodLike<T = any> {
|
|
149
|
-
parse(value: unknown): T
|
|
150
|
-
_def: unknown
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Type-level extraction: if Z is a Zod schema, infer its output type */
|
|
154
|
-
type InferZodOutput<Z> = Z extends { parse(value: unknown): infer T; _def: unknown } ? T : never
|
|
155
|
-
|
|
156
|
-
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
157
|
-
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
158
|
-
return (
|
|
159
|
-
typeof value === "object" &&
|
|
160
|
-
value !== null &&
|
|
161
|
-
typeof (value as any).parse === "function" &&
|
|
162
|
-
"_def" in (value as any) &&
|
|
163
|
-
!("~standard" in (value as any))
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
168
|
-
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
169
|
-
return (value: string) => {
|
|
170
|
-
try {
|
|
171
|
-
return schema.parse(value)
|
|
172
|
-
} catch (err: any) {
|
|
173
|
-
// Format Zod errors as Commander-style messages
|
|
174
|
-
if (err?.issues) {
|
|
175
|
-
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
176
|
-
throw new Error(messages)
|
|
177
|
-
}
|
|
178
|
-
throw err
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// --- Typed Command ---
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* A Commander Command with inferred option and argument types.
|
|
187
|
-
* Wraps Commander's Command and tracks option types at the type level.
|
|
188
|
-
* Help is automatically colorized.
|
|
189
|
-
*
|
|
190
|
-
* @typeParam Opts - Accumulated option types from .option() calls
|
|
191
|
-
* @typeParam Args - Accumulated argument types from .argument() calls (tuple)
|
|
192
|
-
*/
|
|
193
|
-
export class TypedCommand<Opts = {}, Args extends any[] = []> {
|
|
194
|
-
readonly _cmd: BaseCommand
|
|
195
|
-
|
|
196
|
-
constructor(name?: string) {
|
|
197
|
-
this._cmd = new BaseCommand(name)
|
|
198
|
-
colorizeHelp(this._cmd as any)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Set program description */
|
|
202
|
-
description(str: string, argsDescription?: Record<string, string>): this {
|
|
203
|
-
this._cmd.description(str, argsDescription as any)
|
|
204
|
-
return this
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Set program version */
|
|
208
|
-
version(str: string, flags?: string, description?: string): this {
|
|
209
|
-
this._cmd.version(str, flags, description)
|
|
210
|
-
return this
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Add an option with type inference.
|
|
215
|
-
*
|
|
216
|
-
* Supports five overload patterns:
|
|
217
|
-
* 1. `.option(flags, description?)` — type inferred from flags syntax
|
|
218
|
-
* 2. `.option(flags, description, defaultValue)` — removes `undefined` from type
|
|
219
|
-
* 3. `.option(flags, description, parser, defaultValue?)` — type inferred from parser return type
|
|
220
|
-
* 4. `.option(flags, description, standardSchema)` — type inferred from Standard Schema output
|
|
221
|
-
* 5. `.option(flags, description, zodSchema)` — type inferred from Zod schema output (legacy, pre-3.24)
|
|
222
|
-
*/
|
|
223
|
-
option<const F extends string, S extends StandardSchemaV1>(
|
|
224
|
-
flags: F,
|
|
225
|
-
description: string,
|
|
226
|
-
schema: S,
|
|
227
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: InferStandardSchema<S> }, Args>
|
|
228
|
-
|
|
229
|
-
option<const F extends string, Z extends ZodLike>(
|
|
230
|
-
flags: F,
|
|
231
|
-
description: string,
|
|
232
|
-
schema: Z,
|
|
233
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: InferZodOutput<Z> }, Args>
|
|
234
|
-
|
|
235
|
-
option<const F extends string, P extends (value: string, previous: any) => any>(
|
|
236
|
-
flags: F,
|
|
237
|
-
description: string,
|
|
238
|
-
parseArg: P,
|
|
239
|
-
defaultValue?: ReturnType<P>,
|
|
240
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: ReturnType<P> }, Args>
|
|
241
|
-
|
|
242
|
-
option<const F extends string, D = undefined>(
|
|
243
|
-
flags: F,
|
|
244
|
-
description?: string,
|
|
245
|
-
defaultValue?: D,
|
|
246
|
-
): TypedCommand<AddOption<Opts, F, D>, Args>
|
|
247
|
-
|
|
248
|
-
option(flags: string, description?: string, parseArgOrDefault?: any, defaultValue?: any): any {
|
|
249
|
-
if (isStandardSchema(parseArgOrDefault)) {
|
|
250
|
-
;(this._cmd as any).option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
251
|
-
} else if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
252
|
-
;(this._cmd as any).option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
253
|
-
} else if (typeof parseArgOrDefault === "function") {
|
|
254
|
-
;(this._cmd as any).option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
255
|
-
} else {
|
|
256
|
-
;(this._cmd as any).option(flags, description ?? "", parseArgOrDefault)
|
|
257
|
-
}
|
|
258
|
-
return this
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Add a required option */
|
|
262
|
-
requiredOption<const F extends string>(
|
|
263
|
-
flags: F,
|
|
264
|
-
description?: string,
|
|
265
|
-
defaultValue?: string,
|
|
266
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: FlagValueType<F> }, Args> {
|
|
267
|
-
;(this._cmd as any).requiredOption(flags, description ?? "", defaultValue)
|
|
268
|
-
return this as any
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Add an option with a fixed set of allowed values (choices).
|
|
273
|
-
* The option type is narrowed to a union of the provided values.
|
|
274
|
-
*
|
|
275
|
-
* @example
|
|
276
|
-
* ```ts
|
|
277
|
-
* .optionWithChoices("-e, --env <env>", "Environment", ["dev", "staging", "prod"] as const)
|
|
278
|
-
* // → env: "dev" | "staging" | "prod" | undefined
|
|
279
|
-
* ```
|
|
280
|
-
*/
|
|
281
|
-
optionWithChoices<const F extends string, const C extends readonly string[]>(
|
|
282
|
-
flags: F,
|
|
283
|
-
description: string,
|
|
284
|
-
choices: C,
|
|
285
|
-
): TypedCommand<Opts & { [K in ExtractLongName<F>]: C[number] | undefined }, Args> {
|
|
286
|
-
const option = new Option(flags, description).choices(choices as unknown as string[])
|
|
287
|
-
;(this._cmd as any).addOption(option)
|
|
288
|
-
return this as any
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Add a subcommand */
|
|
292
|
-
command(nameAndArgs: string, description?: string): TypedCommand<{}> {
|
|
293
|
-
const sub = (this._cmd as any).command(nameAndArgs, description)
|
|
294
|
-
colorizeHelp(sub as any)
|
|
295
|
-
const typed = new TypedCommand<{}>()
|
|
296
|
-
// Replace the internal command with the one Commander created
|
|
297
|
-
;(typed as any)._cmd = sub
|
|
298
|
-
return typed
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Add an argument with type tracking.
|
|
303
|
-
* `<name>` = required (string), `[name]` = optional (string | undefined).
|
|
304
|
-
*/
|
|
305
|
-
argument<const N extends string>(
|
|
306
|
-
name: N,
|
|
307
|
-
description?: string,
|
|
308
|
-
defaultValue?: unknown,
|
|
309
|
-
): TypedCommand<Opts, [...Args, ArgType<N>]> {
|
|
310
|
-
this._cmd.argument(name, description ?? "", defaultValue)
|
|
311
|
-
return this as any
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Set action handler with typed parameters.
|
|
316
|
-
* Callback receives: ...arguments, opts, command.
|
|
317
|
-
*/
|
|
318
|
-
action(fn: (...args: [...Args, Prettify<Opts>, TypedCommand<Opts, Args>]) => void | Promise<void>): this {
|
|
319
|
-
this._cmd.action(fn as any)
|
|
320
|
-
return this
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Get typed parsed options */
|
|
324
|
-
opts(): Prettify<Opts> {
|
|
325
|
-
return this._cmd.opts() as any
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Parse argv */
|
|
329
|
-
parse(argv?: readonly string[], options?: { from?: "node" | "electron" | "user" }): this {
|
|
330
|
-
this._cmd.parse(argv as any, options as any)
|
|
331
|
-
return this
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Parse argv async */
|
|
335
|
-
async parseAsync(argv?: readonly string[], options?: { from?: "node" | "electron" | "user" }): Promise<this> {
|
|
336
|
-
await this._cmd.parseAsync(argv as any, options as any)
|
|
337
|
-
return this
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** Get help text */
|
|
341
|
-
helpInformation(): string {
|
|
342
|
-
return this._cmd.helpInformation()
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** Allow unknown options */
|
|
346
|
-
allowUnknownOption(allow?: boolean): this {
|
|
347
|
-
this._cmd.allowUnknownOption(allow)
|
|
348
|
-
return this
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Allow excess arguments */
|
|
352
|
-
allowExcessArguments(allow?: boolean): this {
|
|
353
|
-
this._cmd.allowExcessArguments(allow)
|
|
354
|
-
return this
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Pass through options after -- */
|
|
358
|
-
passThroughOptions(passThrough?: boolean): this {
|
|
359
|
-
this._cmd.passThroughOptions(passThrough)
|
|
360
|
-
return this
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Enable positional options */
|
|
364
|
-
enablePositionalOptions(positional?: boolean): this {
|
|
365
|
-
this._cmd.enablePositionalOptions(positional)
|
|
366
|
-
return this
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Hook into lifecycle events */
|
|
370
|
-
hook(event: string, listener: (...args: any[]) => void | Promise<void>): this {
|
|
371
|
-
;(this._cmd as any).hook(event, listener)
|
|
372
|
-
return this
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Set custom name */
|
|
376
|
-
name(str: string): this {
|
|
377
|
-
this._cmd.name(str)
|
|
378
|
-
return this
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** Add alias */
|
|
382
|
-
alias(alias: string): this {
|
|
383
|
-
this._cmd.alias(alias)
|
|
384
|
-
return this
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Add multiple aliases */
|
|
388
|
-
aliases(aliases: readonly string[]): this {
|
|
389
|
-
this._cmd.aliases(aliases as string[])
|
|
390
|
-
return this
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/** Configure help display */
|
|
394
|
-
configureHelp(config: Record<string, unknown>): this {
|
|
395
|
-
;(this._cmd as any).configureHelp(config)
|
|
396
|
-
return this
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** Configure output streams */
|
|
400
|
-
configureOutput(config: Record<string, unknown>): this {
|
|
401
|
-
;(this._cmd as any).configureOutput(config)
|
|
402
|
-
return this
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** Access underlying Commander Command for advanced use */
|
|
406
|
-
get commands(): readonly BaseCommand[] {
|
|
407
|
-
return this._cmd.commands
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Show help */
|
|
411
|
-
help(context?: { error?: boolean }): never {
|
|
412
|
-
return (this._cmd as any).help(context) as never
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/** Add help text */
|
|
416
|
-
addHelpText(position: "before" | "after" | "beforeAll" | "afterAll", text: string): this {
|
|
417
|
-
this._cmd.addHelpText(position, text)
|
|
418
|
-
return this
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/** Show help after error */
|
|
422
|
-
showHelpAfterError(displayHelp?: boolean | string): this {
|
|
423
|
-
this._cmd.showHelpAfterError(displayHelp)
|
|
424
|
-
return this
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** Show suggestion after error */
|
|
428
|
-
showSuggestionAfterError(displaySuggestion?: boolean): this {
|
|
429
|
-
this._cmd.showSuggestionAfterError(displaySuggestion)
|
|
430
|
-
return this
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/** Set environment variable for the last added option (passthrough) */
|
|
434
|
-
env(name: string): this {
|
|
435
|
-
// Commander's .env() is on Option, not Command. We apply it to the last option.
|
|
436
|
-
const opts = (this._cmd as any).options as any[]
|
|
437
|
-
if (opts.length > 0) {
|
|
438
|
-
opts[opts.length - 1].envVar = name
|
|
439
|
-
opts[opts.length - 1].envVarRequired = false
|
|
440
|
-
}
|
|
441
|
-
return this
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Create a typed, colorized CLI program.
|
|
447
|
-
*
|
|
448
|
-
* @example
|
|
449
|
-
* ```ts
|
|
450
|
-
* import { createCLI } from "@silvery/commander"
|
|
451
|
-
*
|
|
452
|
-
* const program = createCLI("myapp")
|
|
453
|
-
* .description("My tool")
|
|
454
|
-
* .version("1.0.0")
|
|
455
|
-
* .option("-v, --verbose", "Verbose output")
|
|
456
|
-
* .option("-p, --port <number>", "Port", parseInt)
|
|
457
|
-
*
|
|
458
|
-
* program.parse()
|
|
459
|
-
* const { verbose, port } = program.opts()
|
|
460
|
-
* // ^boolean ^number | undefined
|
|
461
|
-
* ```
|
|
462
|
-
*/
|
|
463
|
-
export function createCLI(name?: string): TypedCommand<{}> {
|
|
464
|
-
return new TypedCommand(name)
|
|
465
|
-
}
|