@silvery/commander 0.3.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 +107 -127
- package/package.json +6 -18
- package/src/colorize.ts +164 -0
- package/src/command.ts +121 -0
- package/src/index.ts +11 -173
- package/src/presets.ts +41 -36
- package/src/z.ts +44 -0
- package/src/typed.ts +0 -465
package/README.md
CHANGED
|
@@ -1,198 +1,178 @@
|
|
|
1
1
|
# @silvery/commander
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
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.
|
|
4
6
|
|
|
5
7
|
## Usage
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
8
|
-
import {
|
|
10
|
+
import { Command, port, csv } from "@silvery/commander"
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
.description("
|
|
12
|
+
new Command("deploy")
|
|
13
|
+
.description("Deploy the application")
|
|
12
14
|
.version("1.0.0")
|
|
13
|
-
.option("-
|
|
14
|
-
.option("
|
|
15
|
-
.option("-
|
|
16
|
-
.option("--no-color", "Disable color output")
|
|
17
|
-
|
|
18
|
-
cli.parse()
|
|
19
|
-
const { verbose, port, output, color } = cli.opts()
|
|
20
|
-
// ^boolean ^number ^string|true ^boolean
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Help output is automatically colorized using Commander's built-in `configureHelp()` style hooks (headings bold, flags green, commands cyan, descriptions dim, arguments yellow).
|
|
24
|
-
|
|
25
|
-
You can also use `colorizeHelp()` standalone with a plain Commander `Command`:
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
import { Command } from "commander"
|
|
29
|
-
import { colorizeHelp } from "@silvery/commander"
|
|
15
|
+
.option("-p, --port <n>", "Port", port)
|
|
16
|
+
.option("--tags <t>", "Tags", csv)
|
|
17
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
colorizeHelp(program) // applies recursively to all subcommands
|
|
19
|
+
program.parse()
|
|
33
20
|
```
|
|
34
21
|
|
|
35
|
-
|
|
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.
|
|
36
23
|
|
|
37
|
-
|
|
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`).
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
import { createCLI, port, csv, int, url, oneOf } from "@silvery/commander"
|
|
41
|
-
|
|
42
|
-
const cli = createCLI("deploy")
|
|
43
|
-
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
44
|
-
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
45
|
-
.option("--tags <t>", "Tags", csv) // string[]
|
|
46
|
-
.option("--callback <url>", "Callback", url) // string (validated URL)
|
|
47
|
-
.option("-e, --env <e>", "Env", oneOf(["dev", "staging", "prod"])) // "dev" | "staging" | "prod"
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Standalone usage
|
|
26
|
+
## Validated options with built-in types
|
|
51
27
|
|
|
52
|
-
|
|
28
|
+
Commander's `.option()` accepts a string and gives you a string back. Our built-in types parse and validate in one step:
|
|
53
29
|
|
|
54
30
|
```typescript
|
|
55
|
-
import { port, csv,
|
|
31
|
+
import { Command, port, csv, int } from "@silvery/commander"
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
|
|
33
|
+
new Command("deploy")
|
|
34
|
+
.option("-p, --port <n>", "Port", port) // number (1-65535, validated)
|
|
35
|
+
.option("--tags <t>", "Tags", csv) // string[]
|
|
36
|
+
.option("-r, --retries <n>", "Retries", int) // number (integer)
|
|
37
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"]) // choices
|
|
38
|
+
```
|
|
59
39
|
|
|
60
|
-
|
|
61
|
-
const result = port.safeParse("abc")
|
|
62
|
-
// { success: false, issues: [{ message: 'Expected port (1-65535), got "abc"' }] }
|
|
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.
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
const validated = port["~standard"].validate("8080")
|
|
66
|
-
// { value: 8080 }
|
|
67
|
-
```
|
|
42
|
+
### Available types
|
|
68
43
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
### Factory presets
|
|
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
|
|
87
60
|
|
|
88
61
|
```typescript
|
|
89
|
-
import { intRange
|
|
62
|
+
import { intRange } from "@silvery/commander"
|
|
90
63
|
|
|
91
|
-
intRange(1, 100)
|
|
92
|
-
oneOf(["a", "b", "c"]) // Preset<"a" | "b" | "c"> — enum from values
|
|
64
|
+
intRange(1, 100) // CLIType<number> -- integer within bounds
|
|
93
65
|
```
|
|
94
66
|
|
|
95
|
-
|
|
67
|
+
### Array choices
|
|
96
68
|
|
|
97
|
-
|
|
69
|
+
Pass an array as the third argument to restrict an option to a fixed set of values:
|
|
98
70
|
|
|
99
71
|
```typescript
|
|
100
|
-
|
|
101
|
-
.option("-p, --port <n>", "Port", parseInt) // → port: number
|
|
102
|
-
.option("-t, --timeout <ms>", "Timeout", Number) // → timeout: number
|
|
103
|
-
.option("--tags <items>", "Tags", (v) => v.split(",")) // → tags: string[]
|
|
72
|
+
.option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
104
73
|
```
|
|
105
74
|
|
|
106
|
-
|
|
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.:
|
|
107
80
|
|
|
108
81
|
```typescript
|
|
109
|
-
|
|
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: "..." }] }
|
|
110
88
|
```
|
|
111
89
|
|
|
90
|
+
The `/parse` subpath has zero dependencies -- no Commander, no [Zod](https://github.com/colinhacks/zod).
|
|
91
|
+
|
|
112
92
|
## Standard Schema validation
|
|
113
93
|
|
|
114
|
-
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema)
|
|
94
|
+
Pass any [Standard Schema v1](https://github.com/standard-schema/standard-schema) schema as the third argument to `.option()`. This works with [Zod](https://github.com/colinhacks/zod) (>=3.24), [Valibot](https://github.com/fabian-hiller/valibot) (>=1.0), [ArkType](https://github.com/arktypeio/arktype) (>=2.0), and any library implementing the protocol:
|
|
115
95
|
|
|
116
96
|
```typescript
|
|
97
|
+
import { Command } from "@silvery/commander"
|
|
117
98
|
import { z } from "zod"
|
|
118
99
|
|
|
119
|
-
|
|
100
|
+
new Command("deploy")
|
|
120
101
|
.option("-p, --port <n>", "Port", z.coerce.number().min(1).max(65535))
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.option("-e, --env <env>", "Environment", z.enum(["dev", "staging", "prod"]))
|
|
124
|
-
// → env: "dev" | "staging" | "prod" (union type)
|
|
125
|
-
|
|
102
|
+
.option("-e, --env <env>", "Env", z.enum(["dev", "staging", "prod"]))
|
|
126
103
|
.option(
|
|
127
104
|
"--tags <t>",
|
|
128
105
|
"Tags",
|
|
129
106
|
z.string().transform((v) => v.split(",")),
|
|
130
107
|
)
|
|
131
|
-
// → tags: string[] (transformed)
|
|
132
108
|
```
|
|
133
109
|
|
|
134
|
-
Schema libraries are optional peer dependencies -- detected at runtime
|
|
110
|
+
Schema libraries are optional peer dependencies -- detected at runtime, never imported at the top level.
|
|
135
111
|
|
|
136
|
-
##
|
|
112
|
+
## Zod CLI types
|
|
137
113
|
|
|
138
|
-
|
|
114
|
+
Import `z` from `@silvery/commander` for [Zod](https://github.com/colinhacks/zod) extended with CLI-specific schemas:
|
|
139
115
|
|
|
140
116
|
```typescript
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.option("-
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
117
|
+
import { Command, z } from "@silvery/commander"
|
|
118
|
+
|
|
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"])
|
|
148
124
|
```
|
|
149
125
|
|
|
150
|
-
|
|
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.
|
|
151
127
|
|
|
152
|
-
|
|
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)`.
|
|
153
129
|
|
|
154
|
-
|
|
130
|
+
## Function parsers
|
|
155
131
|
|
|
156
|
-
|
|
157
|
-
const cli = createCLI("deploy").optionWithChoices("-e, --env <env>", "Environment", ["dev", "staging", "prod"] as const)
|
|
158
|
-
// → env: "dev" | "staging" | "prod" | undefined
|
|
132
|
+
[Commander's](https://github.com/tj/commander.js) standard parser function pattern also works:
|
|
159
133
|
|
|
160
|
-
|
|
161
|
-
|
|
134
|
+
```typescript
|
|
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)
|
|
162
139
|
```
|
|
163
140
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
## Environment variable support
|
|
141
|
+
## colorizeHelp()
|
|
167
142
|
|
|
168
|
-
|
|
143
|
+
Use standalone with a plain [Commander](https://github.com/tj/commander.js) `Command` (without subclassing):
|
|
169
144
|
|
|
170
145
|
```typescript
|
|
171
|
-
|
|
146
|
+
import { Command } from "commander"
|
|
147
|
+
import { colorizeHelp } from "@silvery/commander"
|
|
148
|
+
|
|
149
|
+
const program = new Command("myapp")
|
|
150
|
+
colorizeHelp(program) // applies recursively to all subcommands
|
|
172
151
|
```
|
|
173
152
|
|
|
174
|
-
##
|
|
175
|
-
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
153
|
+
## Import paths
|
|
154
|
+
|
|
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 |
|
|
159
|
+
|
|
160
|
+
## Beyond extra-typings
|
|
161
|
+
|
|
162
|
+
Built on the shoulders of [@commander-js/extra-typings](https://github.com/commander-js/extra-typings). We add:
|
|
163
|
+
|
|
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
|
|
188
169
|
|
|
189
170
|
## Credits
|
|
190
171
|
|
|
191
|
-
- [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
|
|
193
|
-
- [Standard Schema](https://github.com/standard-schema/standard-schema) -- universal schema interop protocol
|
|
194
|
-
- [@silvery/ansi](https://github.com/beorn/silvery/tree/main/packages/ansi) -- optional
|
|
195
|
-
- 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
|
|
196
176
|
|
|
197
177
|
## License
|
|
198
178
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Commander Command with auto-colorized help, Standard Schema support,
|
|
3
|
+
* and array-as-choices detection.
|
|
4
|
+
*
|
|
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()`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Command, port, csv } from "@silvery/commander"
|
|
12
|
+
*
|
|
13
|
+
* new Command("deploy")
|
|
14
|
+
* .option("-p, --port <n>", "Port", port)
|
|
15
|
+
* .option("--tags <t>", "Tags", csv)
|
|
16
|
+
* .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
|
|
17
|
+
*
|
|
18
|
+
* program.parse()
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Command as BaseCommand, Option } from "commander"
|
|
23
|
+
import { colorizeHelp } from "./colorize.ts"
|
|
24
|
+
import type { StandardSchemaV1 } from "./presets.ts"
|
|
25
|
+
|
|
26
|
+
// --- Standard Schema support ---
|
|
27
|
+
|
|
28
|
+
/** Runtime check: is this value a Standard Schema v1 object? */
|
|
29
|
+
function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
30
|
+
return typeof value === "object" && value !== null && "~standard" in (value as any)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Wrap a Standard Schema as a Commander parser function */
|
|
34
|
+
function standardSchemaParser<T>(schema: StandardSchemaV1<T>): (value: string) => T {
|
|
35
|
+
return (value: string) => {
|
|
36
|
+
const result = schema["~standard"].validate(value)
|
|
37
|
+
if ("issues" in result) {
|
|
38
|
+
const msg = result.issues.map((i) => i.message).join(", ")
|
|
39
|
+
throw new Error(msg)
|
|
40
|
+
}
|
|
41
|
+
return result.value
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Legacy Zod support (pre-3.24, no ~standard) ---
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Duck-type interface for older Zod schemas that don't implement Standard Schema.
|
|
49
|
+
* Any object with `parse(value: string) => T` and `_def` qualifies.
|
|
50
|
+
*/
|
|
51
|
+
interface ZodLike<T = any> {
|
|
52
|
+
parse(value: unknown): T
|
|
53
|
+
_def: unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Runtime check: is this value a legacy Zod-like schema (without Standard Schema)? */
|
|
57
|
+
function isLegacyZodSchema(value: unknown): value is ZodLike {
|
|
58
|
+
return (
|
|
59
|
+
typeof value === "object" &&
|
|
60
|
+
value !== null &&
|
|
61
|
+
typeof (value as any).parse === "function" &&
|
|
62
|
+
"_def" in (value as any) &&
|
|
63
|
+
!("~standard" in (value as any))
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Wrap a legacy Zod schema as a Commander parser function */
|
|
68
|
+
function legacyZodParser<T>(schema: ZodLike<T>): (value: string) => T {
|
|
69
|
+
return (value: string) => {
|
|
70
|
+
try {
|
|
71
|
+
return schema.parse(value)
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
// Format Zod errors as Commander-style messages
|
|
74
|
+
if (err?.issues) {
|
|
75
|
+
const messages = err.issues.map((i: any) => i.message).join(", ")
|
|
76
|
+
throw new Error(messages)
|
|
77
|
+
}
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class Command extends BaseCommand {
|
|
84
|
+
constructor(name?: string) {
|
|
85
|
+
super(name)
|
|
86
|
+
colorizeHelp(this as any)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add an option with smart third-argument detection.
|
|
91
|
+
*
|
|
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
|
|
98
|
+
*/
|
|
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
|
+
}
|
|
105
|
+
if (isStandardSchema(parseArgOrDefault)) {
|
|
106
|
+
return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault))
|
|
107
|
+
}
|
|
108
|
+
if (isLegacyZodSchema(parseArgOrDefault)) {
|
|
109
|
+
return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault))
|
|
110
|
+
}
|
|
111
|
+
if (typeof parseArgOrDefault === "function") {
|
|
112
|
+
return super.option(flags, description ?? "", parseArgOrDefault, defaultValue)
|
|
113
|
+
}
|
|
114
|
+
return super.option(flags, description ?? "", parseArgOrDefault)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Subcommands also get colorized help, Standard Schema, and array choices
|
|
118
|
+
createCommand(name?: string): Command {
|
|
119
|
+
return new Command(name)
|
|
120
|
+
}
|
|
121
|
+
}
|